Building a Radio Button Tree using Angular FormArray

I have found a radio button tree structure in many web applications. This story shows one of the many ways you could implement it.

Below the data we are going to use. Its an array of objects exported from books.ts

//books.ts
export const books={
“books”:[{
“type”:”Horror”,”authors”:[
{“name”:”Stephen King”,”books”:[“Revival”,”Dark Half”,”Pet Sematary”,”Misery”,”The Girl who loved Tom Gordon”]},{“name”:”Dan Brown”,”books”:[“Origin”,”The Da Vinci Code”,”Digital Fortress”,”Angels and Demons”,”Deception Point”]},{“name”:”Katie Macalister”,”books”:[“Dragon Soul”,”Fire Born”,”Shadow Born”,”Life,Love and the Pursuit of Hotties”,”Time Crossed”]}
]
},
{
“type”:”Science and Fiction”,”authors”:[
{“name”:”Jules Verne”,”books”:[“Twenty Thousand Leagues Under the Sea”,”Journey to the center of the Earth”,”From the Earth to the Moon”,”Around the Moon”,”Off On a Comet”]},{“name”:”H.G Wells”,”books”:[“The Time Machine”,”The War of the Worlds”,”The invisible man”,”The First Men in the Moon”,”The Sleeper awakes”]},{“name”:”Arthur C. Clark”,”books”:[“Childhood’s End”,”The Sentinel”,”The City and the Stars”,”The Final Oddyssey”,”Rendezvous with Rama”]}
]
}]}

Each object describes the type of book,few authors who have written books on that genre and the books they have written.

Template:

<form [formGroup]=”radioForm” (ngSubmit)=”submit(radioForm)”>
<ng-container formArrayName=”booksArray”>
<ul>
<li *ngFor=”let x of radioForm.get(‘booksArray’).controls;let i=index;” [formGroupName]=”i”><i id=”bookType{{i}}” appClick elemIndex={{i}} class=”fa fa-angle-double-right”></i>
<input type="text" formControlName="type">
<ng-container formArrayName=”authors”>
<ul class=”close” id=”AuthorDetails{{i}}”>
<li *ngFor=”let y of returnAuthors(x);
let j=index;”[formGroupName]=”j”
>
<i id=”bookAuthor{{i}}{{j}}” appClick elemIndex={{i}} elemIndexJ={{j}} class=”fa fa-angle-double-right”></i>
<input type="text" formControlName="name">
<ul class=”close” id=”BooksList{{i}}{{j}}”>
<li *ngFor=”let z of returnBooks(y)”>
<input type=”radio” formControlName=”select” [value]=”z”>{{z}}
</li>
</ul>
</li>
</ul>
</ng-container>
</li>
</ul>
</ng-container>
<button type=”submit”>Submit</button>
</form>
Radio Button Tree

We have an array named books which contains 2 objects. The above snippet shows 1 object of the 2.

  1. So the above books array corresponds to the below FormArray booksArray.
<ng-container formArrayName=”booksArray”>

2. Each object in the books array contains 2 properties:type and authors. type is a string and authors is an array of objects.

So each object is a FormGroup,type string corresponds to the type FormControl and the authors array corresponds to the authors FormArray.

<li *ngFor=”let x of radioForm.get(‘booksArray’).controls;let i=index;” [formGroupName]=”i”><i id=”bookType{{i}}” appClick elemIndex={{i}} class=”fa fa-angle-double-right”></i>
<input type="text" formControlName="type">
<ng-container formArrayName=”authors”>

3. In the authors array, we again have objects with 2 properties:name and books. name is a string and books is an array of strings.

So each object is a FormGroup,name string corresponds to the name FormControl and the books array corresponds to the select FormArray.

<li *ngFor=”let y of returnAuthors(x);
let j=index;”[formGroupName]=”j”
>
<i id=”bookAuthor{{i}}{{j}}” appClick elemIndex={{i}} elemIndexJ={{j}} class=”fa fa-angle-double-right”></i>
<input type="text" formControlName="name">
<ul class=”close” id=”BooksList{{i}}{{j}}”>
<li *ngFor=”let z of returnBooks(y)”>
<input type=”radio” formControlName=”select” [value]=”z”>{{z}}

Component Class:

import { books } from 'src/books';export class RadioTreeComponent implements OnInit {
constructor() { }
data={…books};
radioForm=new FormGroup({
booksArray:new FormArray([])})
ngOnInit() {
for(let x in this.data.books){
(<FormArray>this.radioForm.get(‘booksArray’)).push(new FormGroup({
type:new FormControl(this.data.books[x].type),
authors:new FormArray(this.loadAuthors(this.data.books[x].authors))
}))
}
}
submit(form){
console.log(form.value);
}
returnAuthors(x){
return x.get(‘authors’).controls;
}
returnBooks(x){
return x.get(‘books’).value
}
loadAuthors(arr){
let controls=[];
for(let x in arr){
controls.push(new FormGroup({
name:new FormControl(arr[x].name),
books:new FormControl(arr[x].books),
select:new FormControl(“”)
}))
}
return controls;
}}

Let’s begin.

The data we have exported from books.ts is an object containing an array of objects. Thus we shall construct the array first using FormArray named booksArray as you see below. The array is initialised to [].

radioForm=new FormGroup({
booksArray:new FormArray([])})

Once the component is loaded,we have written the logic to construct the FormGroups inside this FormArray.

ngOnInit() {
for(let x in this.data.books){
(<FormArray>this.radioForm.get(‘booksArray’)).push(new FormGroup({
type:new FormControl(this.data.books[x].type),
authors:new FormArray(this.loadAuthors(this.data.books[x].authors))
}))
}
}

data is a variable holding the exported object. data.books holds the array of objects.

We have iterated through this array and for each object in the array we are adding a new FormGroup into the FormArray.

Each FormGroup contains a FormControl named type and a FormArray named authors.

FormControl type contains the type of the book and FormArray authors is again an array of objects, where each object contains the name of the author and books written by the author.

The FormArray value is returned by the loadAuthors(). We are passing the array of author details as argument to the method.

loadAuthors(arr){
let controls=[];
for(let x in arr){
controls.push(new FormGroup({
name:new FormControl(arr[x].name),
books:new FormControl(arr[x].books),
select:new FormControl(“”)
}))
}
return controls;
}

In this method, we have created an array named controls and initialised to [].

For every object in the array containing the array details(passed as argument), we are pushing a new FormGroup into the controls array.

Each FormGroup contains 3 FormControls: name which tells us about the name of the author and books is an array of books written by the author.

select is the FormControl of the radio button and the value of each radio button will be the names of each of the books in books FormControl.

The below 2 functions help in retrieving the controls property of the FormArray authors and the value of the FormControl books. Using a method to retrieve the value is a workaround to avoid errors when trying to access the properties directly in the template.

returnAuthors(x){
return x.get(‘authors’).controls;
}
returnBooks(x){
return x.get(‘books’).value
}

When I check a few radio buttons and submit the form, the output will be like below. The selected book for an author will be visible under select property.

On Submitting the Form

In order to toggle the visibility of the authors and books in the Tree, we have created a Directive below which does the job. We should be able to hide/show the authors whenever we click on the type of book and hide/show book list whenever we click on the author name.

@Directive({selector: ‘[appClick]’})export class ClickDirective {
constructor(private element:ElementRef,private renderer:Renderer2) { }
@Input(‘elemIndex’) elemIndex:number;
@Input(‘elemIndexJ’) elemIndexJ:number;
@HostListener(‘click’)
toggleDisplay(){
let element_id=this.element.nativeElement.id;
let target_element:any;
if(element_id.indexOf(‘bookType’) > -1){
target_element=document.querySelector(‘#AuthorDetails’+this.elemIndex);
this.toggle(target_element);
}
else if(element_id.indexOf(‘bookAuthor’) > -1){
target_element=document.querySelector(‘#BooksList’+this.elemIndex+this.elemIndexJ);
this.toggle(target_element);
}
}
toggle(target_element){
if(target_element.className===’close’){
this.renderer.removeClass(target_element,’close’);
this.renderer.addClass(target_element,’open’);
}
else if(target_element.className===’open’){
this.renderer.removeClass(target_element,’open’);
this.renderer.addClass(target_element,’close’);
}}}

The directive has been applied to the below 2 <i> tags in the template.

<i id=”bookType{{i}}” appClick elemIndex={{i}} class=”fa fa-angle-double-right”></i><i id=”bookAuthor{{i}}{{j}}” appClick elemIndex={{i}} elemIndexJ={{j}} class=”fa fa-angle-double-right”></i>

So whenever I click on any of these 2 elements, the toggleDisplay() of the directive is called. This method uses the ID of the clicked element and the classes of the target list to decide whether to hide or show the list.

The Directive uses the below classes:

.open{display:block;}.close{display: none;}

Check the entire working below:

An Angular Developer who loves finding answers to all the mysteries of this universe