18
loading...
This website collects cookies to deliver better user experience
Material UI => Tree View Component UI
GraphQL and Apollo Client => Fetch data from back4app database
npm install @mui/lab @mui/material @mui/icons-material @apollo/client graphql
ApolloProvider
to be available in all you app.import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
// URI for graphql API on back4app
const httpLink = createHttpLink({
uri: "https://parseapi.back4app.com/graphql",
});
const headersLink = setContext((_, { headers }) => {
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
// These keys are found when you create app on back4app
"X-Parse-Application-Id": "<YOUR_APPLICATION_ID>",
"X-Parse-Master-Key": "<YOUR_MASTER_KEY>",
"X-Parse-REST-API-Key": "<YOUR_REST_API_KEY>",
},
};
});
const client = new ApolloClient({
link: headersLink.concat(httpLink),
cache: new InMemoryCache(),
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root")
);
import { gql } from "@apollo/client";
export const GET_CONTINENTS = gql`
query allContinents {
data: continentscountriescities_Continents {
count
results: edges {
node {
objectId
name
children: countries {
count
}
}
}
}
}
`;
export const GET_COUNTRIES = gql`
query allCountries($continentId: ID) {
data: continentscountriescities_Countries(
where: { continent: { have: { objectId: { equalTo: $continentId } } } }
) {
count
results: edges {
node {
objectId
name
children: cities {
count
}
}
}
}
}
`;
export const GET_CITIES = gql`
query allCities($countryId: ID) {
data: continentscountriescities_Cities(
where: { country: { have: { objectId: { equalTo: $countryId } } } }
) {
count
results: edges {
node {
objectId
name
}
}
}
}
`;
gql
String literal provided by apollo client will help in the validation of your query against the main schema.CustomTreeItem
would look something like this.import React, { useEffect } from "react";
import clsx from "clsx";
import { CircularProgress, Typography } from "@mui/material";
import TreeItem, { useTreeItem } from "@mui/lab/TreeItem";
import { useLazyQuery } from "@apollo/client";
import { GET_COUNTRIES, GET_CITIES } from "../../utils/Queries";
const CustomContent = React.forwardRef(function CustomContent(
props,
ref
) {
// TreeItemContentProps + typename + appendNewData props
const {
classes,
className,
label,
nodeId,
icon: iconProp,
expansionIcon,
displayIcon,
typename,
appendNewData,
} = props;
// Extract last part from Typename key of node from graphql
// Ex: Continentscountriescities_Country => Country
const type: string = typename?.split("_")[1] || "";
let lazyQueryParams = {};
// Add lazyQueryParams according to type of node
switch (type) {
case "Continent":
lazyQueryParams = {
query: GET_COUNTRIES,
variableName: "continentId",
};
break;
case "Country":
lazyQueryParams = {
query: GET_CITIES,
variableName: "countryId",
};
break;
default:
lazyQueryParams = {
query: GET_COUNTRIES,
variableName: "continentId",
};
break;
}
// Lazy query for getting children of this node
const [getChildren, { loading, data }] = useLazyQuery(
lazyQueryParams?.query,
{
variables: { [lazyQueryParams?.variableName]: nodeId },
}
);
const { disabled, expanded, selected, focused, handleExpansion } =
useTreeItem(nodeId);
const icon = iconProp || expansionIcon || displayIcon;
// Append new children to node
useEffect(() => {
if (data?.data?.results && appendNewData) {
appendNewData(nodeId, data.data?.results || []);
}
}, [data]);
const handleExpansionClick = (event) => {
// Fetch data only once
if (!data) {
getChildren();
}
handleExpansion(event);
};
return (
<div
className={clsx(className, classes.root, {
[classes.expanded]: expanded,
[classes.selected]: selected,
[classes.focused]: focused,
[classes.disabled]: disabled,
})}
onClick={handleExpansionClick}
ref={ref}
>
<div className={classes.iconContainer}>{icon}</div>
<Typography component="div" className={classes.label}>
{label}
</Typography>
</div>
);
});
const CustomTreeItem = (props) => {
return (
<TreeItem
ContentComponent={CustomContent}
// These props will be sent from the parent
ContentProps={
{ typename: props.typename, appendNewData: props.appendNewData } as any
}
{...props}
/>
);
};
export default CustomTreeItem;
useLazyQuery
hook from apollo client, we have a method getChildren()
(or any other name) to be called wherever and whenever we need in the component. So we are calling this method on the handleExpansionClick
method and checking if the data is not already fetched.import React, { useEffect, useState } from "react";
import { useQuery } from "@apollo/client";
import TreeView from "@mui/lab/TreeView";
import { CircularProgress } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { GET_CONTINENTS } from "../../utils/Queries";
import CustomTreeItem from "../CustomTreeItem";
import { getModifiedData } from "../../utils/Shared";
const Tree = () => {
// Get all continents on first render
const { loading, data: allContinents } = useQuery(GET_CONTINENTS);
// Data to render all tree items from
const [treeItemsData, setTreeItemsData] = useState([]);
// Set treeItemsData with continents recieved
useEffect(() => {
if (allContinents?.data?.results) {
setTreeItemsData(allContinents?.data?.results);
}
}, [allContinents]);
// Add new data in its correct place in treeItemsData array
const appendNewData = (nodeId, data) => {
const treeItemsDataClone = JSON.parse(JSON.stringify(treeItemsData)); // Deep Copy
// getModifiedData is the recursive function (will be shown below alone)
const newData = getModifiedData(treeItemsDataClone, nodeId, data);
setTreeItemsData(newData); // set the rendered array with the modified array
};
// Render children items recursively
const renderChild = (node) => {
return (
<CustomTreeItem
key={node.objectId}
classes={{ content: styles.treeItemContent }}
typename={node.__typename}
appendNewData={appendNewData}
nodeId={node.objectId}
label={node.name}
>
{/* If children is an object with a count key > 0, render a dummy treeItem to show expand icon on parent node */}
{node.children &&
(node.children.count > 0 ? (
<CustomTreeItem nodeId="1" />
) : (
node.children.length &&
node.children.map((child: any) => renderChild(child.node)) // Recursively rendering children if array is found
))}
</CustomTreeItem>
);
};
// Show a loader until query resolve
if (loading) return <CircularProgress />;
else if (allContinents)
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{treeItemsData.map((continent: any) => {
return renderChild(continent.node);
})}
</TreeView>
);
else return <></>;
};
export default Tree;
/*
Original Answer: https://stackoverflow.com/a/15524326
@Description: Searches for a specific object in nested objects or arrays according to "objectId" key
@Params: originalData => The original array or object to search in
nodeId => the id to compare to objectId field
dataToBeAdded => new data to be added ad children to found node
@Returns: Modified original data
*/
export const getModifiedData = (
originalData: any,
nodeId: string,
dataToBeAdded: any
) => {
let result = null;
const originalDataCopy = JSON.parse(JSON.stringify(originalData)); // Deep copy
if (originalData instanceof Array) {
for (let i = 0; i < originalDataCopy.length; i++) {
result = getModifiedData(originalDataCopy[i], nodeId, dataToBeAdded);
if (result) {
originalDataCopy[i] = result;
}
}
} else {
for (let prop in originalDataCopy) {
if (prop === "objectId") {
if (originalDataCopy[prop] === nodeId) {
originalDataCopy.children = dataToBeAdded;
return originalDataCopy;
}
}
if (
originalDataCopy[prop] instanceof Object ||
originalDataCopy[prop] instanceof Array
) {
result = getModifiedData(originalDataCopy[prop], nodeId, dataToBeAdded);
if (result) {
originalDataCopy[prop] = result;
break;
}
}
}
}
return originalDataCopy;
};