Wednesday, May 18, 2022

Loading Saves Nodes in Angular

Part 2: Loading Saved Nodes

Welcome to the second and final installment of this web development tutorial series on Recursive Tree Node Processing. In part one, we wrote code to save vehicle manufacturer, model, and level selections from the VehicleNode tree. The save operation entailed the creation of a simplified tree by whittling it down to those nodes which were selected. In today’s follow-up, we will add the other half of the functionality to our demo app to apply saved selections to the matTree control. This exercise will require us to match saved ids to those in the TREE_DATA in order to programmatically select saved nodes.

If you missed the first part of this Angular tutorial or need a refresher, we suggest you read: Recursive Tree Node Processing in Angular.

Invoking the loadSelection() Method

Recall that, in the demo app, vehicle selections are persisted to, and recalled from, a list of links, which can be seen above the MatTree control:

Tree Angular Examples

 

Clicking a link invokes the loadSelection() method, rather than navigating to another page, as most links do. There are a few stodgy web developers who admonish those who would dare use links to trigger actions rather than navigation. To them, I say pshaw!

The trick is to set the href attribute to “javascript:void(0)” and then bind our method to the click handler. We will pass it the array index so that we know which selection was clicked:

<div class="saved-selections">Load selection:<br/> 
  <ul>
  <li *ngFor="let savedSelection of savedSelections; let i=index">
      <a role="button" href="javascript:void(0)" (click)="loadSelection(i)">{{savedSelection.name}}</a><br/>
  </li>
</ul>
</div>

Read: Filter DOM Nodes Using a Tree Walker

Clearing the Current Selection with deselctAllNodes()

Before loading a previous configuration of selected nodes, we must first deselect any and all nodes that are already selected. Otherwise, we would end up with extra selections. This responsibility is handed off to the recursive deselectAllNodes() method. As we have seen in Part One, we need to invoke the recursive method within a loop that iterates over every root node, since each of these represents a separate tree:

public loadSelection(index: number) {
  this.deselectAllNodes(this.dataSource.data);

  // Toggle the selected nodes...
}

The deselectAllNodes() method targets all selected and indeterminate nodes and toggles them off. Then it does the same for each node’s children:

private deselectAllNodes(vehicleNodes: VehicleNode[]) {
  vehicleNodes.forEach(node => {
    if(node.selected) {
      node.selected = false;
    }
    if (node.children) {
      if(node.indeterminate) {
        node.indeterminate = false;
      }
      this.deselectAllNodes(node.children);
    }
  });
}

Once all nodes have been deselected, the saved selections are retrieved by index, and the toggleSelectedNodes() method goes to work on activating nodes in the MatTree according to the stored IDs in the savedSelections’ selections attribute:

public loadSelection(index: number) {
  this.dataSource.data.forEach(node => {
    this.deselectAllNodes(node);
  });
  const savedSelections = this.savedSelections[index];
  this.toggleSelectedNodes(
    savedSelections.selections, 
    this.dataSource.data
  );
}

As to be expected, toggleSelectedNodes() also processes nodes in a recursive manner. Since selections store node ids as far down each tree branch that is necessary to identify selected nodes, toggleSelectedNodes() must also follow each branch until there are no more children. This becomes apparent when we review an example SavedSelection object:

{
  name: "Infiniti Selections",
  selections: [{
    id: "infiniti",
    children: [
      {
        id: "g50",
        children: [ { id: 2 } ]
      },
      { id: "qx50" }
    ]
  }]
}

The first step is to match the stored id with that of a VehicleNode. If both the vehicleSelection and vehicleNode have children, the method is again applied to each object’s children. Once we have reached the leaf node of the vehicleSelection, it is time to call itemToggle() on the corresponding vehicleNode. The great thing about that approach is that itemToggle() will take care of selecting all of the node’s ancestors for us! If any part of the VehicleNodes object does not match up with the saved VehicleSelections, a warning is output to the console while the method proceeds to go through the remaining VehicleSelections:

private toggleSelectedNodes(
  vehicleSelections: VehicleSelection[],
  vehicleNodes: VehicleNode[]
) {
  vehicleSelections.forEach(selection => {
    const vehicleNode = vehicleNodes.find(
      vehicleNode => vehicleNode.id === selection.id
    );
    if (vehicleNode) {
      if (selection.children) {
        if (vehicleNode.children) {
          this.toggleSelectedNodes(
            selection.children, 
            vehicleNode.children
          );
        } else {
          console.warn(`Node with id '${vehicleNode.id}' has no children.`)
        }
      } else {
        this.itemToggle(true, vehicleNode);
      }
    } else {
      console.warn(`Couldn't find vehicle node with id '${selection.id}'`)
    }
  });
}

Here is the updated demo that includes the new selection loading functionality.

Conclusion

And that brings us to the end of this two-part series. In the first installment, we covered 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 this article, we reconstituted the original VehicleNodes tree from saved selections.

Read more Angular web development tutorials.

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

Popular Articles

Featured