Skip to main content

Using Angular @ViewChild to implement multiple visualizations of data sets

Sometimes, a single representation of data is just not sufficient.
If we take a music library app as an example, one would want to look at the music collection either in list form, or as a pretty list of the albums’ cover images while keeping the rest of the UI unchanged. Both are using the same underlying data set, and ideally, the change in presentation happens without reloading the data from the server every time.
In Angular, the @ViewChild can be used to implement that scenario, and in this post, we’re going to develop an example where the user can switch between multiple different representations of the data, like this:
Two alternatives for displaying the same data set.

Overall Angular architecture

To keep things tidy, we’re going to organize the components and views for the different list representations in directories under a folder album-listfor the AlbumListComponent. Here is an image of the final folder structure, showing all the files we’re going to create and use in this example:
The folder structure used in this implementation.
The code structure is set up so that the AlbumListComponent is going to be responsible for loading the data and providing an entry point for the page as well as the UI, with the exception of the actual lists not being rendered here.
The logic for the switching of the views is going to be accomplished by the BaseListComponent, which will do the heavy lifting of rendering and switching between user-selected views and passing any data to them.
The use of two separate components neatly separates the presentation of the page in the AlbumListComponent from the business logic of managing the rendering of the different, selected views, located in the BaseListComponent. Down the line, we’re going to create the actual implementations for the rendering of the list in specialized list render components, such as the NumberedListComponent and CoverListComponent shown in the diagram below.
Relationship between the various components shown in the previous screenshot.
Let’s start with the outermost album-list.component.html template.

Step 1: Data retrieval and UI setup

Following is the HTML we’re using on the album list page to allow a user to switch between different representations of the list of albums.
<div class="albumlist-container">
  <div class="format-selector">
    <button type="button"
       (click)="onChangeAlbumDisplayClick('list')">List</button>
    <button type="button"
       (click)="onChangeAlbumDisplayClick('covers')">Covers</button>
  </div>
</div>
Here, we are using the component’s onChangeAlbumDisplayClick() function that will change the object property determining which list view will be rendered. The corresponding Typescript code of the component looks like this (excluding the @Component decorator):
export class AlbumListComponent {  public currentStyle = 'covers';
  public albumList: Album[] = [];  public onChangeAlbumDisplayClick($event) {
    this.currentStyle = $event;
  }
}
As we can see, the component has two properties, currentStyle and albumList, which are used to store the list of albums and the style in which to display them. Both will be passed to a child BaseListComponent we’re going to develop shortly.
The basic UI is now ready, so we can embed the BaseListComponent into the template and pass the two data properties, so the logic can react to any changes. With this addition, the album-list.component.html looks like this:
<div class="albumlist-container">
  <div class="format-selector">
    <button type="button"
      (click)="onChangeAlbumDisplayClick('list')">List</button>
    <button type="button"
      (click)="onChangeAlbumDisplayClick('covers')">Covers</button>
    </div>
    <div>
        <base-list [showContentStyle]="currentStyle" 
                   [albumList]="albumList"></base-list>
    </div>
</div>
We see that in the template, we’re including a tag <base-list/>, for the BaseListComponent. That component will be orchestrating the rendering of the actual display components for the lists and contain the code to swap the view components for the data dynamically.
Here’s the code for the AlbumListComponent class (again without @Component decorator)
export class AlbumListComponent implements OnInit {  // The two properties, which are passed to BaseListComponent via property binding 
  public currentStyle = 'covers';
  public albumList: Album[] = [];  constructor(private albumService: AlbumService) { }  // Loading the album data from the server
  ngOnInit() {
    this.albumService.getAllAlbums()
      .subscribe(
        (albumList: Album[]) => this.albumList = albumList;
      );
  }  public onChangeAlbumDisplayClick($event) {
    this.currentStyle = $event;
  }
}
All that has been added is the call to the server via the AlbumService and the storing of the retrieved data in the albumList property. For the purposes of this example, I’m going to skip the handling of errors in the API call.

Step 2: List Component and view switching

All the work we’ve done so far was to create a UI which allows the user to load and choose how to display the data. Before we get to the creation of the components rendering the data in different ways, we will look at the BaseListComponent which will make the dynamic rendering of the selected view component happen.
To start out, let’s create the component and hook it up with the inputs it will receive from the AlbumListComponent:
@Component({
  selector: 'base-list',
  templateUrl: './base-list.component.html',
})
export class BaseListComponent implements OnInit {  @Input() showContentStyle: string;
  @Input() albumList: Album[] = [];  private contentStyles = {
    list: NumberedListComponent,
    covers: CoverListComponent,
  };  ngOnInit() {
    this.showContentStyle = 'covers';
    this.instantiateViewComponent(this.showContentStyle);
  }  private instantiateViewComponent(componentName: string) {}
}
We see the two @Input() lines at the beginning to allow the component to accept the property bindings of the AlbumListComponent. After the @Input()we define contentStyles, a mapping from the string identifying the component (“list” or “covers”) to the corresponding component class being rendered later.
This setup is followed by the ngOnInit()function, which sets the default view and calls a method, which will render and insert the selected component into the template of the BaseListComponent. The template for this component is really short and simple and consists only of a <div /> element with an identifier we’re going to use later to insert the rendered component (#albumListStyle) at that location of the template:
<div>
  <div #albumListStyle></div>
</div>
With this basic setup, we’re ready to tackle the heart of the implementation, the instantiateViewComponent() method receiving the string identifying the component to instantiate. This method needs to accomplish the following tasks:
The following code accomplishes these tasks:
@ViewChild('albumListStyle', {read: ViewContainerRef, static: true})
  albumListContainer: ViewContainerRef;  constructor(
    private factoryResolver: ComponentFactoryResolver,
  ) { }  private componentReference: ComponentRef<{}>;  private instantiateViewComponent(componentName: string) {
    const componentType = this.provideListComponent(componentName);
    const factoryInstance =
        this.factoryResolver.resolveComponentFactory(componentType);
    this.componentReference =
        this.albumListContainer.createComponent(factoryInstance);    const inst = this.componentReference.instance as AbstractList;
    inst.albumListToRender = this.albumList;
  }  private provideListComponent(componentStyle: string) {
    return this.contentStyles[componentStyle] ||
           this.contentStyles.list;
  }
}
In the first two lines, the @ViewChilddecorator creates a reference to the HTML element in this component’s template with the #albumListStyleannotation and stores the reference in the albumListContainer property.
In line 11, the provideListComponent()method is used to identify and return a reference to the component class associated with the provided string identifier componentName — or to otherwise return a default. Lines 12 and 13 retrieve the selected component’s factory and instantiate the component from it. Notice also in line 9, how we’re referring to the ViewChild via the albumListContainer property to create and inject the component. A reference to the newly instantiated component is kept in the componentReference object property, so that we can do two things:
To pass any data into the child component, we get a reference to the class instance and provide the loaded data to the rendering component, which is shown in lines 11 and 12. We have not yet seen the components doing the actual rendering, but we can take an educated guess from this code that they possess an albumListToRender property. Before we go into the components doing the actual rendering, let’s look again at our overview graphic and see what we’ve accomplished so far:

Step 3: Implementing the list views

We saw in the previous section that we’re passing data to the components taking care of the rendering of the list. In line 15 of the last listing, you may have noticed a reference to an AbstractList. That (abstract) class is the basis for all components tasked with rendering lists. It provides a common data reference and looks as follows:
export abstract class AbstractList {
  public albumListToRender: Album[];
}
For a real implementation of a list we look at the component rendering the albums in form of a grid of cards with the album covers, artist and album name. The Typescript class is very straight forward:
@Component({
  selector: 'app-cover-list',
  templateUrl: './cover-list.component.html',
  styleUrls: ['./cover-list.component.css']
})
export class CoverListComponent extends AbstractList {
  constructor() {
    super();
  }
}
It extends and instantiates the AbstractList class in its constructor method, thereby gaining access to the albumListToRender property. The corresponding template is straight forward Angular templating:
<div>
  <div *ngFor="let album of albumListToRender" 
       class="albumcard-container">
    <div class="albumcard-cover"><!-- image tag here--></div>
    <div class="albumcard-data">
      <h3>{{ album.albumName }}</h3>
      <h4>{{ album.artist }}</h4>
    </div>
  </div>
</div>
Each of the components rendering a view have a very similar setup:
With this knowledge, setting up new views merely involves creating a new component and implementing the HTML to render the album data in the required output format. That, and adding the new component to the contentStyles property in the BaseListComponent.

Step 4: Cleaning up and dealing with view changes

The implementation so far almost works. There is still a problem with the rendering of the components displaying the list and a problem that an existing component is not removed when the view should be changed. To solve these two problems, we’re going to use the ngOnChange and ngOnDestroy methods in the BaseListComponent:
export class BaseListComponent implements OnInit, OnDestroy, OnChanges {  ...  ngOnDestroy() {
    this.destroyChildComponent();
  }  ngOnChanges() {
    this.destroyChildComponent();
    this.instantiateViewComponent(this.showContentStyle);
  }  private destroyChildComponent() {
    if (this.componentReference) {
      this.componentReference.destroy();
      this.componentReference = null;
    }
  }
}
The ngOnDestroy() method unloads (destroys) the dynamically rendered component to avoid memory leaks through this implementation. The ngOnChanges() method will get called at each change of the showContentStyleproperty, indicating that the user requested a different view. This method is responsible for destroying the already existing view component and render the new view.

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...