body {
  margin: 0;
  padding: 16px;
  font-size: 16px;
  font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
}

.tree {
  gap: 8px;
  display: grid;
  grid-auto-rows: max-content;
  
  &-item {
    outline: 1px solid #333333;
    
    &:hover {
      outline: 2px solid rgb(65 105 225);
    }
    
    &__content {
      padding: 8px;
      
      &:hover {
        background-color: rgb(65 105 225 / 20%);
        box-shadow: inset 0 0 0 4px #ffffff;
      }
    }
    
    &__list {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    
    &__inner {
      padding-left: 24px;
      gap: 8px;
      display: grid;
      grid-auto-rows: max-content;
    }
  }
}
View Compiled
interface Item {
  id: string;
  parentId: null | string;
  categoriesIds: string[];
  title: string;
  userId: string;
}

const itemList: Item[] = [
  {
    id: "i1",
    parentId: null,
    categoriesIds: ["c8"],
    title: "rubber",
    userId: "u81"
  },
  {
    id: "i2",
    parentId: null,
    categoriesIds: ["c2"],
    title: "tragedy",
    userId: "u76"
  },
  {
    id: "i3",
    parentId: "i2",
    categoriesIds: [],
    title: "ease",
    userId: "u81"
  },
  {
    id: "i4",
    parentId: "i3",
    categoriesIds: [],
    title: "round",
    userId: "u65"
  },
  {
    id: "i5",
    parentId: "i4",
    categoriesIds: ["c3"],
    title: "ladybug",
    userId: "u54"
  },
  {
    id: "i6",
    parentId: "i3",
    categoriesIds: ["c20"],
    title: "spinach",
    userId: "u14"
  },
  {
    id: "i7",
    parentId: "i3",
    categoriesIds: ["c23", "c10"],
    title: "impact",
    userId: "u48"
  },
  {
    id: "i8",
    parentId: "i7",
    categoriesIds: ["c30"],
    title: "clerk",
    userId: "u51"
  },
  {
    id: "i9",
    parentId: "i8",
    categoriesIds: ["c7", "c5", "c16", "c28"],
    title: "dot",
    userId: "u23"
  }
];

type Branch<T> = {
  data: T;
  children: Branch<T>[];
};

type Tree<T> = Branch<T>[];

const createTree = <T, K>(
  collection: T[],
  extractId: (entry: T) => K,
  extractParentId: (entry: T) => K | null
): Tree<T> => {
  const cache = new Map<K, Branch<T>>();
  const tree: Tree<T> = [];

  for (const entry of collection) {
    const id = extractId(entry);
    const parentId = extractParentId(entry);
    const branch: Branch<T> = {
      data: entry,
      children: []
    };

    const root =
      parentId !== null ? cache.get(parentId)?.children ?? tree : tree;
    root.push(branch);
    cache.set(id, branch);
  }

  return tree;
};

const tree = createTree(
  itemList,
  (entry) => entry.id,
  (entry) => entry.parentId
);

const visualizeTreeInner = <T,>(tree: Tree<T>, root) => {
  for (const { data, children } of tree) {
    const template = `
      <div class="tree-item">
        <div class="tree-item__content">
          <ul class="tree-item__list">
            ${Object.entries(data).map(([key, value]) => `
              <li><strong>${key}</strong> – <code>${JSON.stringify(value)}</code></li>
            `).join('')}
          </ul>
        </div>
        <div class="tree-item__inner"></div>
      </div>
    `;
    const wrapper = document.createElement('div');
    wrapper.innerHTML = template;
    const element = wrapper.children[0];
    
    visualizeTreeInner(children, element.querySelector('.tree-item__inner'));
    
    root.append(element);
  }
};

const visualizeTree = <T,>(tree: Tree<T>) => {
  const wrapper = document.createElement('div');
  wrapper.className = 'tree';
  
  visualizeTreeInner(tree, wrapper);
  
  return wrapper;
}

document.body.append(
  visualizeTree(tree)
);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.