I recently ran into a requirement to show a nested list of items in a menu for a “code explorer” component: Show There’s no concrete limit to how deep this list can go (each item can have its own list of items and so on and so forth). Here’s the data that drives this UI:
Notice how each instance of So how can we render this in Blazor? Well it turns out Blazor can handle a little bit of recursion, making this relatively straightforward. First we’ll want a root component, to act as the starting point for the TreeView.
All the magic happens in the Node.razor
The first step is to render the UI for the current node (in this case, a button). The next step is to loop over each of the sub nodes and render instances of the Make sure you forward any parameters along to the nested
If you forgot to set the With that, we have a potentially infinite depth Tree View component. From here we can extend this to handle things like showing different icons for different node types, simply by making changes to that Next upInject content into your Blazor components with typed Render FragmentsMaintain clear separation between your components and “inject” the markup you need Using .NET 7’s Blazor Custom Elements to render dynamic contentRender Blazor components from dynamic content, at runtime? Wait until the last responsible moment to add structure to your Blazor UIPrioritise the ability to iterate and evolve your UI
If you look for them, you WIll find tree structures all over the place – from nature, to cities and their neighborhoods, to your own family’s genealogy (i.e., your family tree). Unsurprisingly, trees crop up just as often in programmatic data structures; for instance, in the Document Object Model (DOM):
In coding languages such as JavaScript and TypeScript, trees are usually represented using an array of arrays and/or objects whose keys are comprised of other arrays and/or objects. A common technique employed when working with tree structures is recursion, whereby a function calls itself. While not the only solution, in many cases, it is the simplest approach. Sound complicated? Fear not: recursion is actually relatively easy to get the hang of, once you have worked through it a couple of times. To that end, in this two-part web development tutorial series, we will be mapping a complex tree to a more simplistic one, and then back again. In this first installment, we will cover the basic structure of the recursive function and then put it to use to persist the array of VehicleNodes that was introduced in the Tracking Selections with Checkboxes in Angular article back in December. In part 2, we will reconstitute the original VehicleNodes tree from the saved selections. Trees vs. Tree Arrays in AngularFor the purposes of review, here is the VehicleNode interface and tree that we will be working with in this series: interface VehicleNode { name: string; id: number | string; children?: VehicleNode[]; selected?: boolean; indeterminate?: boolean; parent?: VehicleNode; } const TREE_DATA: VehicleNode[] = [ { name: 'Infiniti', children: [ { name: 'G50', children: [ { name: 'Pure AWD', id: 1 }, { name: 'Luxe', id: 2 }, ], }, { name: 'QX50', children: [ { name: 'Pure AWD', id: 3 }, { name: 'Luxe', id: 4 }, ], }, ], }, { name: 'BMW', children: [ { name: '2 Series', children: [ { name: 'Coupé', id: 5 }, { name: 'Gran Coupé', id: 6 }, ], }, { name: '3 Series', children: [ { name: 'Sedan', id: 7 }, { name: 'PHEV', id: 8 }, ], }, ], }, ]; Technically, all trees have a trunk – that is to say, a single root node. In your family tree, that node would be you. In the above structure, that would be each automobile manufacturer, i.e., Infiniti and BMW. Hence, we can think of our data structure as an array of trees, rather than a single tree. This should become fairly obvious if we unpack the array and reconstruct it using the separate tree objects: const infiniti = { name: 'Infiniti', children: [ { name: 'G50', children: [ { name: 'Pure AWD', id: 1 }, { name: 'Luxe', id: 2 }, ], }, { name: 'QX50', children: [ { name: 'Pure AWD', id: 3 }, { name: 'Luxe', id: 4 }, ], }, ], }; const bmw = { name: 'BMW', children: [ { name: '2 Series', children: [ { name: 'Coupé', id: 5 }, { name: 'Gran Coupé', id: 6 }, ], }, { name: '3 Series', children: [ { name: 'Sedan', id: 7 }, { name: 'PHEV', id: 8 }, ], }, ], }; const TREE_DATA: VehicleNode[] = [infiniti, bmw]; It is well worth distinguishing between the two, as their handling differs slightly. Read: Manipulate DOM Tree Elements without Writing JavaScript Anatomy of a Recursive Function in AngularWhile the exact code of a recursive function will largely depend on what it is designed to do, as well as the exact structure of the tree, we can give a fast and loose approximation of what such a function might look like: function callRecursively(node: Object) { doSomethingWithNode; if (node.hasChildren) { node.children.forEach(childNode => { this. callRecursively(childNode); }); } } To apply the callRecursively() function to an array of tree objects, all that is required is to deal with each tree of the array within a loop. Here is an example of pseudo-code that employs forEach(): TREE_DATA.forEach(node => { callRecursively(node); }); Read: Getting Fancy with the JavaScript FOR Loop Saving Node Selections in AngularTo gain familiarity with the process of developing a recursive function based on the above template, we will refactor the app that we built in the Tracking Selections with Checkboxes in Angular article so that vehicle selections can be persisted to and recalled from a list of links. Here is what the finished app will look like:
In the above image, we can see a list of saved selections at the top of the tree control as well as a button and text input field for saving the current selections. By examining the format of the saved Infiniti Selections, we can glean an understanding of the saved object format: public savedSelections: SavedSelection[] = [ { name: "Infinity Selections", selections: [{ id: "infiniti", children: [ { id: "g50", children: [ { id: 2 } ] }, { id: "qx50" } ] }] }, // ... } The object structure closely resembles that of the TREE_DATA, but with a couple of key differences:
There is only one problem with the current approach: the TREE_DATA does not include ids! Let’s remedy that by adding some now:
Sure, we could use the name field for lookups, but experience dictates that it is never wise to use names for lookups as these can change over time and may require translation at some point. Read: Filter DOM Nodes Using a TreeWalker The save() Function in AngularWith that done, we are ready to implement the save() method. It just does some validation before adding the current node selections to the savedSelections array: public save() { this.errMsg = ''; const saveName = this.saveNameRef.nativeElement.value.trim(); if (saveName === '') { this.errMsg = 'Please provide a save name.'; this.saveNameRef.nativeElement.focus(); } else { this.savedSelections.push({ name: this.sanitizer.sanitize(SecurityContext.HTML, saveName), selections: this.saveSelectedNodes(this.dataSource.data) }); } } The sanitize() method is provided by the DomSanitizer that is injected via the constructor: constructor(private sanitizer: DomSanitizer) { // ... } It cleans up the save name so that it is fit for inserting into the DOM. The selections themselves are collected by the recursive saveSelectedNodes() method. Being a recursive method, it follows the template that was introduced earlier. You can see the recursive call where it passes the node’s children along: private saveSelectedNodes(vehicleNodes: VehicleNode[]): VehicleSelection[] { let vehicleSelections = [] as VehicleSelection[]; vehicleNodes.forEach(node => { if (node.selected || node.indeterminate) { const vehicleSelection: VehicleSelection = { id: node.id }; if (node.children) { vehicleSelection.children = this.saveSelectedNodes(node.children); } vehicleSelections.push(vehicleSelection); } }); return vehicleSelections; } If we add a console.log() just before each vehicleSelection is pushed onto the vehicleSelections array, we can observe the progress as the method iterates over each node:
It works its way from the leaf node on up to each root level entry. Going Forward with DOM Tree StructuresThere is a demo of our application that includes the save functionality. In the next installment, we will add the code to restore saved selections. You can read the next installment here: Loading Saved Nodes in Angular. Read more Angular web development tutorials.
Rob Gravelle Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit. |