Today we will see recursive selects from tree nodes.
Nothing proves the saying “if you want something done right, do it yourself” better than recursion.
Recursion is one of my favorite topics. When a function is recursive, it finds in itself the strength to solve a task.
We can also see recursion as a family affair: a parent function passes to a child function part of the task, which in turn will do the same with the grandson and so on, just as it is in the life cycle (with the theme song of the Lion King in the background).
For our needs today, everything starts with an input json
, which indicates the tree that the select mechanism must have.
Recursive selects from flat json
The most compact structure to manage is the flat / linear one, with an array of objects that have information about which parent is inside them. For a more advanced version we will see how it is possible to manipulate a possible json in a tree structure.
[
{ qid: '1', answer: 'Hardware', parent: null, question: 'what' },
{ qid: '2', answer: 'Software', parent: null, question: 'what' },
{ qid: '3', answer: 'Printer', parent: '1', question: 'for' },
{ qid: '4', answer: 'MsOffice', parent: '2', question: 'for' },
{ qid: '5', answer: 'Photoshop', parent: '2', question: 'for' },
{ qid: '6', answer: 'PC', parent: '1', question: 'for' },
{ qid: '7', answer: 'Charger', parent: '6', question: 'and' },
{ qid: '8', answer: 'Corrupted charger', parent: '7' },
{ qid: '9', answer: 'Excel', parent: '4', question: 'and' },
{ qid: '10', answer: 'Toner', parent: '3', question: 'and' },
{ qid: '11', answer: 'Broken', parent: '3', question: 'and' }
]
The component that contains the select must have a method that retrieves what will be selected within the select.
This way you can use the found value where you need it.
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
question = 'Need';
readonly flatQuestions = [
{ qid: '1', answer: 'Hardware', parent: null, question: 'what' },
...
{ qid: '11', answer: 'Broken', parent: '3', question: 'and' }
];
changed(value: any) {
console.log(value);
}
}
Now we can talk about the beating heart of the system, the recursive select. Let’s create a component in which we will insert:
changed
The EventEmitter that will take care of passing the information “up” from the children select to the parent select.items
The list of options of all the select, which will be filtered based on the parent (with the filter method). Of this list, only the necessary part will remain within the single select.- Accessory methods for:
- insert labels associated with the select.
- to empty the values of the select children once the parent is changed.
- establish when a select is the last of the chain or when it contains other select children (select leaf or select branch).
// recursive-select.component.ts
import {Component, Input, Output, EventEmitter, OnChanges, OnInit
} from '@angular/core';
@Component({
selector: 'app-recursive-select',
templateUrl: './recursive-select.component.html',
})
export class RecursiveSelectComponent implements OnInit, OnChanges {
@Input() questions: any[];
@Input() question: string;
@Input() parent: string;
private newParent: string;
value: string;
childQuestion: string;
// Used for notifications between select.
@Output() changed = new EventEmitter<string>();
// Search in total array the ranged options for select.
get items() {
return this.questions.filter(
(x: { parent: string }) => x.parent === this.parent
);
}
ngOnInit(): void {}
// Change trigger to set children and value.
change(select) {
this.value = select.value;
if (!this.questions.filter(x => x.parent == select.value).length) {
this.changed.emit(select.value);
}
// Find and set child question.
this.setChildQuestion(select.value);
}
// Get child question from questions and set it (optional).
private setChildQuestion(value) {
const childQuestion = this.questions.find(q => q.qid === value);
if (childQuestion) {
this.childQuestion = childQuestion.question;
}
}
// Last child node notify to the parents of a change.
// Used for external effects.
changeEnd(value) {
this.changed.emit(value);
}
// Empty value and parent on select changes.
ngOnChanges() {
// For select label.
if (this.newParent !== this.parent) {
this.value = '';
this.newParent = this.parent;
}
}
}
The template associated with our component will take care of including the select itself and any child select, inside it.
<!-- recursive-select.component.html -->
<div *ngIf="items.length >= 1">
<label>{{ question }} </label>
<select #select [ngModel]="value" (change)="change(select)" class="mb-2">
<option value="" selected="" disabled="disabled">search...</option>
<option *ngFor="let select of items" [value]="select.qid">{{
select.answer
}}</option>
</select>
<div *ngIf="value">
<app-recursive-select [questions]="questions" [parent]="value" (changed)="changeEnd($event)"
[question]="childQuestion"></app-recursive-select>
</div>
</div>
The final ingredient is the inclusion of the select inside the component where we need it, without forgetting the small changed()
method seen at the beginning, used within the template, as below.
In the tag we will insert the complete json list of the possible values of the select [questions]
, the main parent [parent]
set to null
(the parent select of all has no parents), the method necessary for passing the information on the selection (changed)
and the optional initial question [question]
.
<app-recursive-select
[questions]="this.flatQuestions"
[parent]="null"
(changed)="changed($event)"
[question]="question"
></app-recursive-select>
Recursive selects from tree json
The easiest structure to manage in this case is the tree one, with an array of nodes contained in another node array, up to the leaf nodes.
The shape will be just that of a tree.
We have a tree json object like this:
{
question: 'Need',
nodes: [
{
qid: 1,
question: 'what',
answer: 'Hardware',
nodes: [
{
qid: 3,
answer: 'Printer',
question: 'for',
nodes: [
{
qid: 10,
answer: 'Toner',
nodes: []
}, {
qid: 11,
answer: 'Broken',
nodes: []
}
]
},
{
qid: 6,
answer: 'PC',
question: 'for',
nodes: [
{
qid: 7,
answer: 'Charger',
question: 'and',
nodes: [
{
qid: 8,
answer: 'Corrupted charger',
nodes: []
}
]
}
]
}
]
},
{
qid: 2,
question: 'what',
answer: 'Software',
nodes: [
{
qid: 4,
answer: 'MsOffice',
nodes: []
}, {
qid: 5,
answer: 'Photoshop',
nodes: []
}
]
}
]
}
Like the first type we will insert a changed() method that retrieves what will be selected within the select.
This way you can use the found value where you need it.
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
question = 'Need';
readonly treeQuestions = {
question: 'Need',
nodes: [
...
]
};
changed(value: any) {
console.log(value);
}
}
There is something different regarding the logic of how we retrieve the options list (easier, because the array is ready to use) and the list of child nodes.
// recursive-select-from-tree.component.ts
import {Component, Input, Output, EventEmitter, OnChanges, OnInit} from '@angular/core';
@Component({
selector: 'app-recursive-select-tree',
templateUrl: './recursive-select-tree.component.html'
})
export class RecursiveSelectTreeComponent implements OnInit, OnChanges {
@Input() questions: any;
@Input() parent: string;
private newParent: string;
value: string;
childQuestions: any;
childQuestion: string;
question: string;
// Used for notifications between select.
@Output() changed = new EventEmitter<string>();
// Search in total array the ranged options for select.
get items() {
return this.questions.nodes || [];
}
ngOnInit(): void {
this.question = this.questions.question;
}
// Change trigger to set children and value.
change(select) {
this.value = select.value;
this.childQuestions = this.items.filter(x => x.qid == select.value)[0];
if (!this.childQuestions.nodes.length) {
this.changed.emit(select.value);
}
}
// Last child node notify to the parents of a change.
// Used for external effects.
changeEnd(value) {
this.changed.emit(value);
}
// Empty value and parent on select changes.
ngOnChanges() {
// For select label.
if (this.newParent !== this.parent) {
this.value = '';
this.newParent = this.parent;
}
}
}
The template part is the same as already seen for the recursive select.
<!-- recursive-select.component.html -->
<div *ngIf="items.length > 0">
<label>{{ question }} </label>
<select #select [ngModel]="value" (change)="change(select)" class="mb-2">
<option value="" selected="" disabled="disabled">search...</option>
<option *ngFor="let select of items" [value]="select.qid">{{
select.answer
}}</option>
</select>
<div *ngIf="value">
<app-recursive-select-tree
[questions]="childQuestions"
[parent]="value"
(changed)="changeEnd($event)"></app-recursive-select-tree>
</div>
</div>
Below is an example of what we have seen:
That’s all with the recursive selects from tree nodes.
Try it at home!