Let's create a React File Manager Chapter XIV: File Manager Body
Hasan Zohdy
Posted on September 15, 2022
So we've done a lot of work in the previous chapters in the sidebar and loaders, and now it's time to put some efforts in the file manager body.
Little update in progress bar loader
We'll just display a white background if the progress is 0
.
// LoadingProgressBar.tsx
...
return (
<Progress
size="lg"
value={progress}
striped
// šš» We'll add the styles prop to style the root
styles={{
root: {
backgroundColor: progress === 0 ? "white" : undefined,
},
}}
label={progress > 0 ? `${progress}%` : undefined}
color={mapProgressColor()}
animate
/>
);
Separating The Overlay in its own component
As we know now the overlay appears on the file manager loading state, so we'll create a separate component to be only rendered when the loading happen.
So let's create a new file Content/ContentOverlay.tsx
and add the following code:
// ContentOverlay.tsx
import { LoadingOverlay } from "@mantine/core";
import { useLoading } from "app/file-manager/hooks";
export default function ContentOverlay() {
const isLoading = useLoading();
return <LoadingOverlay visible={isLoading} overlayBlur={2} />;
}
Now let's import it in Content.tsx
.
// Content.tsx
import { Card } from "@mantine/core";
import { ContentWrapper } from "./Content.styles";
import ContentOverlay from "./ContentOverlay"; šš»
export default function Content() {
return (
<>
<Card shadow="sm">
<ContentWrapper>
<ContentOverlay /> šš»
</ContentWrapper>
</Card>
</>
);
}
Nodes List
Now its time for the big part, the nodes list, so let's create a new file Content/NodesList.tsx
That will render all nodes.
But we need to split nodes into two arrays, one for folders and one for files.
// NodesList.tsx
import { Grid } from "@mantine/core";
import { useKernel } from "app/file-manager/hooks";
import { useMemo } from "react";
import { DirectoryNode, FileNode } from "./ContentNode";
export default function NodesList() {
const kernel = useKernel();
const currentDirectoryNode = kernel.currentDirectoryNode;
const [directories, files] = useMemo(() => {
const node = currentDirectoryNode;
if (!node || !node.children?.length) return [[], []];
return [
node.children.filter(node => node.isDirectory),
node.children.filter(node => !node.isDirectory),
];
}, [currentDirectoryNode]);
return (
<>
<Grid>
{directories.map(node => (
<Grid.Col key={node.path} span={2}>
{node.name}
</Grid.Col>
))}
{files.map(node => (
<Grid.Col key={node.path} span={2}>
{node.name}
</Grid.Col>
))}
</Grid>
</>
);
}
We created a memo to list all directories nodes and files nodes as well, but we want to make sure first there is current directory node to be loaded and it has children as well.
Now you should see something like this:
Directory Nodes and File Nodes
So we need to make full control over each node, either in the directory node list or in the file node list, so we'll create two components DirectoryNode
and FileNode
to handle each node, they will be created in Content/ContentNode
directory.
// Content/ContentNode/DirectoryNode.tsx
import { DirectoryNodeProps } from "./ContentNode.types";
export default function DirectoryNode({ node }: DirectoryNodeProps) {
return <>{node.name}</>;
}
// Content/ContentNode/FileNode.tsx
import { FileNodeProps } from "./ContentNode.types";
export default function FileNode({ node }: FileNodeProps) {
return <>{node.name}</>;
}
Let's create ContentNode.types.ts
to hold the types for both components.
// Content/ContentNode/ContentNode.types.ts
import { Node } from "app/file-manager/Kernel";
export type FileNodeProps = {
node: Node;
};
export type DirectoryNodeProps = {
node: Node;
};
All what we added in the props is just the node object.
Now let's create an index file to export all components in the directory.
// Content/ContentNode/index.ts
export { default as DirectoryNode } from "./DirectoryNode";
export { default as FileNode } from "./FileNode";
Now let's import these nodes in our NodesList
// NodesList.tsx
import { Grid } from "@mantine/core";
import { useKernel } from "app/file-manager/hooks";
import { useMemo } from "react";
šš» import { DirectoryNode, FileNode } from "./ContentNode";
export default function NodesList() {
const kernel = useKernel();
const currentDirectoryNode = kernel.currentDirectoryNode;
const [directories, files] = useMemo(() => {
const node = currentDirectoryNode;
if (!node || !node.children?.length) return [[], []];
return [
node.children.filter(node => node.isDirectory),
node.children.filter(node => !node.isDirectory),
];
}, [currentDirectoryNode]);
return (
<>
<Grid>
{directories.map(node => (
<Grid.Col key={node.path} span={2}>
šš» <DirectoryNode node={node} />
</Grid.Col>
))}
{files.map(node => (
<Grid.Col key={node.path} span={2}>
šš» <FileNode node={node} />
</Grid.Col>
))}
</Grid>
</>
);
}
useCurrentDirectoryNode Hook
Let's create a new hook that returns current directory node, and also listen for any change for it, if so then re-render any component that uses this hook.
// hooks/useCurrentDirectoryNode.ts
import { Node } from "app/file-manager/Kernel";
import { useEffect, useState } from "react";
import useKernel from "./useKernel";
export default function useCurrentDirectoryNode() {
const kernel = useKernel();
const [node, setNode] = useState<Node | undefined>(
kernel.currentDirectoryNode,
);
useEffect(() => {
const event = kernel.on("directoryChange", setNode);
return () => event.unsubscribe();
}, [kernel]);
return node;
}
We simply get the current directory node from the kernel and set it in a state
then we watched for any change that happen to current node change, in that case we'll update the state thus the component will be re-rendered.
Using useEvent
There is another good hook to use with Events, useEvent
that will handle the event unsubscribe
for you, so you don't need to worry about it.
// hooks/useCurrentDirectoryNode.ts
šš» import { useEvent } from "@mongez/react";
import { Node } from "app/file-manager/Kernel";
import { useState } from "react";
import useKernel from "./useKernel";
export default function useCurrentDirectoryNode() {
const kernel = useKernel();
const [node, setNode] = useState<Node | undefined>(
kernel.currentDirectoryNode,
);
šš» useEvent(() => kernel.on("directoryChange", setNode));
return node;
}
This will do exactly the same effect as the previous code, but it's more readable and you don't need to worry about the unsubscribe event.
Note that the
useEvent
callback must return the Event Subscription.
Now let's update our NodesList
to listen for changes in the current directory node so it makes a new render.
// NodesList.tsx
import { Grid } from "@mantine/core";
// šš» import the hook
import { useCurrentDirectoryNode } from "app/file-manager/hooks";
import { useMemo } from "react";
import { DirectoryNode, FileNode } from "./ContentNode";
export default function NodesList() {
// šš» remove the useKernel hook and replace it with useCurrentDirectoryNode
ā const kernel = useKernel();
ā const currentDirectoryNode = kernel.currentDirectoryNode;
ā
const currentDirectoryNode = useCurrentDirectoryNode();
const [directories, files] = useMemo(() => {
const node = currentDirectoryNode;
if (!node || !node.children?.length) return [[], []];
return [
node.children.filter(node => node.isDirectory),
node.children.filter(node => !node.isDirectory),
];
}, [currentDirectoryNode]);
return (
<>
<Grid>
{directories.map(node => (
<Grid.Col key={node.path} span={2}>
<DirectoryNode node={node} />
</Grid.Col>
))}
{files.map(node => (
<Grid.Col key={node.path} span={2}>
<FileNode node={node} />
</Grid.Col>
))}
</Grid>
</>
);
}
Enhancing UI for DirectoryNode and FileNode
Now let's make some nice UI for each node, we'll use NavLink
component from Mantine and also some icons as well.
// DirectoryNode.tsx
import { NavLink, useMantineTheme } from "@mantine/core";
import { IconFolder } from "@tabler/icons";
import { useKernel } from "app/file-manager/hooks";
import { DirectoryNodeProps } from "./ContentNode.types";
export default function DirectoryNode({ node }: DirectoryNodeProps) {
// šš» get the theme
const theme = useMantineTheme();
const kernel = useKernel();
return (
// šš» use NavLink component
<NavLink
style={{
// šš» center the node and make the cursor to be default
textAlign: "center",
cursor: "default",
}}
// šš» add the icon, we'll use the IconFolder and give it some good styles from the theme
label={
<>
<IconFolder
fill={theme.colors.blue[4]}
strokeWidth={1.5}
color={theme.colors.blue[9]}
size={40}
/>
<div>{node.name}</div>
</>
}
/>
);
}
Let's do the same in the FileNode
component.
// FileNode.tsx
import { NavLink, useMantineTheme } from "@mantine/core";
import { IconFileInfo as Icon } from "@tabler/icons";
import { FileNodeProps } from "./ContentNode.types";
export default function FileNode({ node }: FileNodeProps) {
const theme = useMantineTheme();
return (
<NavLink
style={{
textAlign: "center",
cursor: "default",
}}
label={
<>
<Icon
fill={theme.colors.green[4]}
strokeWidth={1.5}
color={theme.colors.green[9]}
size={40}
/>
<div>{node.name}</div>
</>
}
/>
);
}
Now the final ui will look like this:
Open Directory on Double Click
Let's add a little feature, when the user double click on the Directory node, let's open it.
// DirectoryNode.tsx
import { NavLink, useMantineTheme } from "@mantine/core";
import { IconFolder } from "@tabler/icons";
import { useKernel } from "app/file-manager/hooks";
import { DirectoryNodeProps } from "./ContentNode.types";
export default function DirectoryNode({ node }: DirectoryNodeProps) {
const theme = useMantineTheme();
const kernel = useKernel();
return (
<NavLink
style={{
textAlign: "center",
cursor: "default",
}}
// šš» add the onDoubleClick event
onDoubleClick={() => kernel.load(node.path)}
label={
<>
<IconFolder
fill={theme.colors.blue[4]}
strokeWidth={1.5}
color={theme.colors.blue[9]}
size={40}
/>
<div>{node.name}</div>
</>
}
/>
);
}
Article Repository
You can see chapter files in Github Repository
Don't forget the
main
branch has the latest updated code.
Tell me where you are now
If you're following up with me this series, tell me where are you now and what you're struggling with, i'll try to help you as much as i can.
Salam.
Posted on September 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.