DEV Community

Cover image for Building Custom Trees in Umbraco 14 using menus!
Yari Mariën
Yari Mariën

Posted on • Edited on

Building Custom Trees in Umbraco 14 using menus!

Table of Contents

  1. Some Thoughts
  2. Building Custom Menu Item Components
  3. Manifests
  4. Final Result

Some Thoughts

There are several methods to create custom trees. One approach is using a full Tree structure, which involves numerous components like ManifestTree, ManifestTreeItem, ManifestTreeStore, DataSource, Repository, and many more files.

File structure Umbraco document section

However, I find this approach overly complex and unnecessary for small, simple trees.

For a basic tree with, I prefer using a menu with custom-rendered menu items using a custom Lit element, as I will demonstrate in this example.

Building Custom Menu Item Components

Rendering Menu Items with Umbraco's UI Menu Item Component

To render your menu items in Umbraco, you can make use of the powerful Umbraco UI Menu Item component. This component allows you to easily create nested menu structures with just a few lines of code.

Example:

<uui-menu-item label="Menu Item 1" has-children>
    <uui-menu-item label="Nested Menu Item 1"></uui-menu-item>
    <uui-menu-item label="Nested Menu Item 2"></uui-menu-item>
</uui-menu-item>
Enter fullscreen mode Exit fullscreen mode

Image of result example code

Example: Fetching and Rendering a Dynamic Tree

In this example, the entire tree is fetched when the component is rendered. Once the data is loaded, we put the items in a @state(). This will trigger a re-render of the tree because it is a reactive property.

With the retrieved data, I dynamically render nested menu items, each with relevant icons. The core functionality of the tree such as opening, closing, and indenting nodes comes from the menu item component itself.

To display the caret icon indicating nested items, you can set the has-children attribute dynamically like this: ?has-children=${bool}.

menu-items.ts:

import { UmbMenuItemElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, TemplateResult } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { FormXTreeItemResponseModel, FormXTreeResource } from '../../../api';


const elementName = 'formx-menu-item';

@customElement(elementName)
class FormXMenuItems extends UmbLitElement implements UmbMenuItemElement {
    @state()
    private _items: FormXTreeItemResponseModel[] = []; // Store fetched items
    @state()
    private _loading: boolean = true; // Track loading state
    @state()
    private _error: string | null = null; // Track any errors

    constructor() {
        super();
        this.fetchInitialItems(); // Start fetching on component load
    }

    // Fetch initial items
    async fetchInitialItems() {
        try {
            this._loading = true;
            this._items = ((await FormXTreeResource.getFormxApiV1Tree()).items); // Fetch root-level items
        } catch (e) {
            this._error = 'Error fetching items';
        } finally {
            this._loading = false;
        }
    }

    // Render items
    renderItems(items: FormXTreeItemResponseModel[]): TemplateResult {
        return html`
            ${items.map(element => html`
                <uui-menu-item label="${element.name}" ?has-children=${element.hasChildren}>
                ${element.type === 1 
                ? html`<uui-icon slot="icon" name="icon-folder"></uui-icon>` 
                : html`<uui-icon slot="icon" name="icon-autofill"></uui-icon>`}
                    ${element.hasChildren ? this.renderItems(element.children) : ''}
                </uui-menu-item>
            `)}
        `;
    }

    // Main render function
    render() {
        if (this._loading) {
            return html`<uui-loader></uui-loader>`;
        }

        if (this._error) {
            return html`<uui-menu-item active disabled label="Could not load form tree!">
        </uui-menu-item>`;
        }

        // Render items if loading is done and no error occurred
        return html`${this.renderItems(this._items)}`;
    }
}



export { FormXMenuItems as element };

declare global {
    interface HTMLElementTagNameMap {
        [elementName]: FormXMenuItems;
    }
}

Enter fullscreen mode Exit fullscreen mode

The result will be a menu that looks like the following:

Result of what our custom element would look like.

Manifests

For this setup, you'll need three key manifests that work together to render a tree using a custom menu.

Let’s start by defining a reusable alias:

const FORMX_MENU_ALIAS = "Our.Umbraco.FormX.menu";
Enter fullscreen mode Exit fullscreen mode

Defining the Tree Space with ManifestSectionSidebarApp

The first manifest defines the space where your tree will appear. This is achieved by creating a ManifestSectionSidebarApp. We can connect the menu to the sidebar using the menu alias.

const sidebarApps: Array<ManifestSectionSidebarApp> = [
    {
        type: 'sectionSidebarApp',
        kind: 'menuWithEntityActions',
        alias: 'Our.Umbraco.FormX.sidebar',
        name: 'FormX Sidebar App',
        meta: {
            label: "Forms",
            menu: FORMX_MENU_ALIAS
        },
        conditions: [
            {
                alias: "Umb.Condition.SectionAlias",
                match: "Our.Umbraco.FormX.section"
            }
        ]
    }
];
Enter fullscreen mode Exit fullscreen mode

Defining the Menu with ManifestMenu

Next, you’ll define the actual menu where your items will be rendered. For this we can use a ManifestMenu.

const menuManifest: Array<ManifestMenu> = [
    {
        type: 'menu',
        alias: FORMX_MENU_ALIAS,
        name: 'FormX Menu'
    }
];
Enter fullscreen mode Exit fullscreen mode

Linking the Custom Elements with ManifestMenuItem

Lastly, we need to link our custom elements that fetch and render your menu items. This is where the ManifestMenuItem comes in.

const menuItemManifest: Array<ManifestMenuItem> = [
    {
        type: 'menuItem',
        alias: 'formx-menu-items',
        name: 'FormX Menu Items',
        meta: {
            label: 'FormX',
            menus: [FORMX_MENU_ALIAS]
        },
        element: () => import('./menu-items.ts')
    }
];
Enter fullscreen mode Exit fullscreen mode

Final Result

Once the SectionSidebarApp is linked to our custom section, the tree will be rendered with the specified label and custom elements.

Example of what our custom section with our newly created custom tree would look like.

Top comments (2)

Collapse
 
kennethsolberg profile image
Kenneth Solberg

Hi, thanks for sharing! The official docs are sadly VERY bad wrt extending the backoffice in v14 so posts like this are really helpful. Let's say you wanted to navigate to a view showing details about a form, how would you do this? Would that be a section view?

Collapse
 
yinzy00 profile image
Yari Mariën

Hi @kennethsolberg,

I'm glad you found it helpful!

Yes, for adding more details about a form, you can definitely use a section view.
I actually had a post on this topic in draft, which I've just published. You can check it out here.