25- React File Manager Chapter XXV: Kernel Tree
Tags: react, typescript, mongez, javascript
Published: 2022-09-20 14:20:27

---
title: 25- React File Manager Chapter XXV: Kernel Tree
published: true
description: 
series: File Manager React Mongez
tags: react, typescript, mongez, javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9pywwf2u56qc6da973mi.png
# Use a ratio of 100:42 for best results.
---

So we stopped at creating a new directory in the server successfully, now let's make a tree based kernel to inject all of our nodes inside it.

But before that let's clean our code a little bit

## Cleaning Our Code

Navigate to `FileManager.tsx` the main component function, and update it with the following code:

```tsx
import { Grid } from "@mantine/core";
import Content from "app/file-manager/components/Content";
import LoadingProgressBar from "app/file-manager/components/LoadingProgressBar";
import Sidebar from "app/file-manager/components/Sidebar";
import Toolbar from "app/file-manager/components/Toolbar";
import { KernelContext } from "app/file-manager/contexts";
import Kernel from "app/file-manager/Kernel";
import { useEffect, useRef } from "react";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";

export default function FileManager({ rootPath }: FileManagerProps) {
  const { current: kernel } = useRef(new Kernel(rootPath as string));

  // load root directory
  useEffect(() => {
    if (!rootPath) return;

    kernel.load(rootPath);
  }, [rootPath, kernel]);

  return (
    <KernelContext.Provider value={kernel}>
      <LoadingProgressBar />
      <Toolbar />
      <BodyWrapper>
        <Grid>
          <Grid.Col span={3}>
            <Sidebar />
          </Grid.Col>
          <Grid.Col span={9}>
            <Content />
          </Grid.Col>
        </Grid>
      </BodyWrapper>
    </KernelContext.Provider>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};
```

What we did here is we removed all defined `states` as it is not needed here anymore and all of our states are now inside the `Kernel` class.

Also we passed the `rootPath` to the kernel class and we're loading the root directory inside the `useEffect` hook.

## Kernel Tree

Now let's add the constructor method to accept the `rootPath` and initialize the `tree` with it.

```tsx
// app/file-manager/Kernel/Kernel.ts
import { Node } from "app/file-manager/Kernel/Node";
...
  /**
   * Constructor
   */
  public constructor(rootPath: string) {
    this.rootPath = rootPath;
  }
```

Now let's define also our `KernelTree` class beside the `Kernel` class.

```tsx
// app/file-manager/Kernel/KernelTree.ts
import Kernel from "./Kernel";
import { Node } from "./Kernel.types";

export default class KernelTree {
  /**
   * Root node
   */
  public root?: Node;

  /**
   * Constructor
   */
  constructor(public kernel: Kernel) {}
}
```

We injected the Kernel to the constructor so we can use any method/property from the kernel directly.

Now let's update our Kernel class to use the `KernelTree` class.

```tsx
// app/file-manager/Kernel/Kernel.ts
import events, { EventSubscription } from "@mongez/events";
import { createDirectory } from "../actions";
import fileManagerService from "../services/file-manager-service";
import { KernelEvents, Node } from "./Kernel.types";
import KernelTree from "./KernelTree";

export default class Kernel {
  ...
  /**
   * Kernel nodes tree
   */
  public tree: KernelTree;

  /**
   * Root node
   */
  public rootNode?: Node;

  /**
   * Constructor
   */
  public constructor(rootPath: string) {
    this.rootPath = rootPath;

    this.tree = new KernelTree(this);
  }
  ...
}
```

## The concept of Kernel Tree

The concept of the kernel tree is to inject all of our nodes inside it, so we can easily access any node directly, update it, delete it or update its children list.

So Our `KernelTree` class will have the following features:

- Set Node: Add the given node to the tree, if the node already exists, then update it.
- Get Node: Get the node by its path or the node itself.
- Delete Node: Delete the node by its path or the node itself.
- Get Parent Node: Get the parent node of the given node.
- Order Node Children: Order the children of the given node by the given order.
- Define node children as as directories and files.

Now let's start implementing the `KernelTree` class.

```tsx
// app/file-manager/Kernel/KernelTree.ts
import Kernel from "./Kernel";
import { Node } from "./Kernel.types";

export default class KernelTree {
  /**
   * Root node
   */
  public root?: Node;

  /**
   * Constructor
   */
  constructor(public kernel: Kernel) {}
  
  /**
   * Set root node
   */
  public setRootNode(root: Node) {
    this.root = root;
    this.kernel.trigger("nodeChange", this.root);

    this.prepareNode(this.root);
  }
}
```

Here we added `setRootNode` to define the top node that will have everything inside it, also we called `prepareNode` which will do two things, to split the node children as directories and files and also order the children by the name alphabetically.

### Preparing Node

As stated earlier, we'll split the node children as directories and files and also order the children by the name alphabetically.

```tsx
// app/file-manager/Kernel/KernelTree.ts

...

  /**
   * Prepare the given node
   */
  public prepareNode(node: Node) {
    if (!node.children) return;

    this.reorderChildren(node);

    // set children directories
    node.directories = node.children.filter(child => child.isDirectory);

    // set children files
    node.files = node.children.filter(child => !child.isDirectory);
  }

  /**
   * Reorder node children by child name
   */
  public reorderChildren(node: Node) {
    node.children?.sort((a, b) => {
      if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) return 1;
      if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) return -1;
      return 0;
    });
  }
```

But the `Node` type does not have `directories` and `files` properties, so let's add them.

```tsx
// app/file-manager/Kernel/Kernel.types.tsx
/**
 * File Manager node is the primary data structure for the File Manager.
 * It can be a directory or a file.
 * It contains the following properties:
 */
export type Node = {
  /**
   * Node Name
   */
  name: string;
  /**
   * Node full path to root
   */
  path: string;
  /**
   * Node size in bits
   */
  size: number;
  /**
   * Is node directory
   */
  isDirectory: boolean;
  /**
   * Node children
   * This should be present (event with empty array) if the node is directory
   */
  children?: Node[];
  /**
   * Get children directories
   */
👉🏻  directories?: Node[];
  /**
   * Get children files
   */
👉🏻  files?: Node[];
};
```

## Updating the Kernel tree

Now we need to update the tree once the node is loaded from the server so let's jump into the `load` method.

```ts
  /**
   * Load the given path
   */
  public load(path: string): Promise<Node> {
    // trigger loading event
    this.trigger("loading");

    return new Promise((resolve, reject) => {
      fileManagerService
        .list(path)
        .then(response => {
          this.currentDirectoryPath = path;

          if (response.data.node.path === this.rootPath) {
            👉🏻 this.tree.setRootNode(response.data.node);
            this.rootNode = response.data.node;
          } else {
            👉🏻 this.tree.setNode(response.data.node);
          }

          // trigger load event as the directory has been loaded successfully.
          this.trigger("load", response.data.node);

          // if the current directory is not as the same loaded directory path,
          // then we'll trigger directory changed event.
          if (response.data.node.path !== this.currentDirectoryNode?.path) {
            this.trigger("directoryChange", response.data.node);
          }

          this.currentDirectoryNode = response.data.node;

          resolve(this.currentDirectoryNode as Node);
        })
        .catch(reject);
    });
  }
```

Now we can update the tree node as we passed the node inside it, so once the node is loaded we check if its the root node, then we update the root node otherwise update the loaded node only.

Let's create `setNode` method inside the `KernelTree` class.

```ts
// app/file-manager/Kernel/KernelTree.ts
  /**
   * Add the given node to the tree
   */
  public setNode(node: Node) {
    // first find the parent node
    let parentNode = this.parentNode(node);

    // if it has no parent, which should not happen, then mark the root as parent
    if (!parentNode) {
      parentNode = this.root;
    }

    // if there is no parent, then do nothing and just return
    if (!parentNode) return;

    // a flag to determine if the given node was already existing but has been changed
    let nodeHasChanged = false;
    // a flag to determine if the parent node is changed
    let parentHasChanged = false;

    // now check if the node already exists in its parent
    if (this.parentHas(parentNode, node)) {
      // if it exists, replace it
      parentNode.children = parentNode.children?.map(child => {
        if (child.path === node.path) {
          if (this.nodeHasChanged(child, node)) {
            nodeHasChanged = true;
            parentHasChanged = true;
          }

          return node;
        }

        return child;
      });
    } else {
      // it means the node does not exist in the parent, then push it to the parent's children
      parentNode?.children?.push(node);
      this.kernel.trigger("newNode", node);
      parentHasChanged = true;
      // prepare the node
      this.prepareNode(node);
    }

    // this will be only triggered if the node has changed
    if (nodeHasChanged) {
      this.prepareNode(node);
      this.kernel.trigger("nodeChange", node);
    }

    // this will be only triggered if the parent node has changed
    if (parentHasChanged) {
      this.prepareNode(parentNode);
      // as the parent node has changed thus the root node will be marked as changed as well
      // we may later make it recursive to mark all the parent nodes as changed
      this.prepareNode(this.root as Node);

      this.kernel.trigger("nodeChange", parentNode);
      this.kernel.trigger("nodeChange", this.root);
    }
  }
```

The code is self explanatory, we check if the node already exists in the parent, if it does then we replace it, otherwise we push it to the parent's children.

Let's add also the following methods:

- `parentNode` to get the parent node of the given node
- `parentHas` to check if the parent node has the given node
- `nodeHasChanged` to check if the given node has changed

```ts
// app/file-manager/Kernel/KernelTree.ts

  /**
   * Check if the given parent has the given node
   */
  public parentHas(parent: Node, node: Node): boolean {
    return parent.children?.some(child => child.path === node.path) ?? false;
  }

  /**
   * Get parent node
   */
  public parentNode(node: Node): Node | undefined {
    return this.findNode(this.getParentPath(node.path));
  }

  /**
   * Find node for the given path recursively in the tree
   */
  public findNode(path: string): Node | undefined {
    // loop starting from the tree root
    const currentNode = this.root;

    const findNode = (node?: Node): Node | undefined => {
      if (node?.path === path) {
        return node;
      }

      if (!node?.children) return undefined;

      for (const child of node.children) {
        const foundNode = findNode(child);

        if (foundNode) return foundNode;
      }
    };

    return findNode(currentNode);
  }

  /**
   * Check if the given node has been changed
   */
  public nodeHasChanged(oldNode: Node, newNode: Node): boolean {
    return JSON.stringify(oldNode) !== JSON.stringify(newNode);
  }

  /**
   * Get the parent path of the given path
   */
  protected getParentPath(path: string): string {
    if (!path) return "/";

    // get the parent path by splitting the path and removing the last item
    return path.split("/").slice(0, -1).join("/");
  }
```

One last thing to do is to update the kernel events

```ts
// Kernel.types

/**
 * Kernel events
 */
export type KernelEvents =
  | "loading"
  | "load"
  | "directoryChange"
  | "nodeChange"
  | "nodeDestroy"
  | "newNode";
```

We added `nodeChange` that will be triggered when a node is changed, `nodeDestroy` that will be triggered when a node is destroyed, and `newNode` that will be triggered when a new node is added.

## Next Chapter

In the next chapter we'll enhance the create directory, the sidebar and the content to watch for node changes.

## Article Repository

You can see chapter files in [Github Repository](https://github.com/hassanzohdy/file-manager-react/tree/25-kernel-tree)

> 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.