Denys Vuika's Blog

Dynamic Material Sidebar in Angular

March 11, 2018

Creating a configuration-based sidebar using Material components in your Angular CLI application.

In this article, we are going to build a Sidebar component. Our component should compile and display its content at runtime. Also, we are going to provide a simple configuration to build sidebar elements in the particular order and with custom settings for each component.

Preparing the project

Let’s create a new project with Angular CLI like the following:

ng new dynamic-sidebar
cd dynamic-sidebar

For all the primitives we are going to use the Angular Material library, which provides us with the Material Design components for Angular.

Let’s quickly go through the setup process for our newly created application. You can get full set of details in the Getting Started section of the Angular Material documentation.

With the command line and Yarn, add the following dependencies to your project:

yarn add @angular/material @angular/cdk @angular/animations

For the Sidebar implementation, we need just the Animations and Expansion modules, so let’s import them to the main application module in the app.module.ts file.

import {
  BrowserAnimationsModule
} from '@angular/platform-browser/animations';

import {
  MatExpansionModule
} from '@angular/material';

@NgModule({
  ...
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatExpansionModule
  ],
  ...
})
export class AppModule { }

For now, let’s use the default indigo-pink theme provided by Angular Material library. You can append the following line to the styles.css file:

@import '~@angular/material/prebuilt-themes/indigo-pink.css';

At this point, your project is ready for hacking. You can test it successfully compiles and runs by executing ng serve --open. Alternatively, you can run yarn start and check the http://localhost:4200 address.

Now let’s get started with the layout for our sidebar component. Update the main application template with the following snippet:

<div class="page">
  <div class="page__sidebar"></div>
  <div class="page__content"></div>
</div>

As you can see from the code above, we got the page element that holds the main content area, and the sidebar to the left.

Below is the minimal set of styles to layout our areas properly:

.page {
  display: flex;
}

.page__sidebar {
  width: 300px;
}

.page__content {
  flex: 1;
}

Finally, let’s put an Expansion Panel wrapped by the Accordion component to our sidebar placeholder.

<mat-accordion>
  <mat-expansion-panel>
    <mat-expansion-panel-header>
      <mat-panel-title>
        Header
      </mat-panel-title>
      <mat-panel-description>
        Description
      </mat-panel-description>
    </mat-expansion-panel-header>

    <p>Panel content goes here</p>
  </mat-expansion-panel>
</mat-accordion>

If you run the application now, the sidebar on the main page should look like in the following picture:

Material Sidebar
Accordion with an expansion panel

Configuration support

For the next step, let’s provide simple configuration support for our component.

You can store your settings as part of the JSON file stored at the server side and fetched by the app upon startup. However, for the sake of simplicity, we are going to store the JSON in the main application container.

Our configuration is going to have an array of panel objects, each containing at least a name and a description.

export class AppComponent {
  config = {
    panels: [
      { name: 'Section 1', description: 'First section' },
      { name: 'Section 2', description: 'Second section' },
      { name: 'Section 3', description: 'Third section' },
    ],
  };
}

Now you can update the template to render Expansion panels dynamically based on the configuration file above:

<mat-accordion>
  <mat-expansion-panel *ngFor="let panel of config.panels">
    <mat-expansion-panel-header>
      <mat-panel-title>
        {{ panel.name }}
      </mat-panel-title>
      <mat-panel-description>
        {{ panel.description }}
      </mat-panel-description>
    </mat-expansion-panel-header>

    <p>Panel content goes here</p>
  </mat-expansion-panel>
</mat-accordion>

This time, upon application start, the sidebar should look similar to the following:

Material Sidebar
Multiple sections from the configuration file

Note, however, that we just render the panel containers at this point. The content of each Expansion panel remains static. We are going to address this later in this article.

Preparing components for the Sidebar

We have created a configuration for three sidebar panels. Now let’s create the corresponding components and store them in the separate SidebarModule module to simplify discovery and reuse.

Use the following Angular CLI command to create a new module and import it into the main application one:

ng g module sidebar --module=app

Next, we need three components to display in the Sidebar. You can also use Angular CLI to create them and save time. Don’t forget to import them into the SidebarModule like in the example below:

ng g component sidebar/sidebar-widget-1 --module=sidebar
ng g component sidebar/sidebar-widget-2 --module=sidebar
ng g component sidebar/sidebar-widget-3 --module=sidebar

Update the sidebar module and add the generated components to the entryComponents section. That is a very important step, it lets Angular know we intend to create the components listed there at runtime.

@NgModule({
  imports: [CommonModule],
  declarations: [
    SidebarWidget1Component,
    SidebarWidget2Component,
    SidebarWidget3Component,
  ],
  entryComponents: [
    SidebarWidget1Component,
    SidebarWidget2Component,
    SidebarWidget3Component,
  ],
})
export class SidebarModule {}

Now we need to return to the configuration settings and update each panel section with an extra component block.

{
  "name": "Section 1",
  "description": "First section",
  "component": {
    "selector": "app-sidebar-widget-1",
    "settings": {}
  }
}

To build a component dynamically, we need to know at least its selector id. You may want to have different components with own configuration settings. So we add an extra settings block to store some widget-related configuration that gets passed to the target component so that it can initialize itself correctly.

Widget Registry

Now we need to build a registry of the widgets. It will be a service that maps selector ids to the real component types. Such an approach allows to support production mode and ahead-of-time (AoT) compilation.

ng g service sidebar/sidebar --module=sidebar

The implementation of the service should look like the following:

import { Injectable, Type } from '@angular/core';
import { SidebarWidget1Component } from './sidebar-widget-1/sidebar-widget-1.component';
import { SidebarWidget2Component } from './sidebar-widget-2/sidebar-widget-2.component';
import { SidebarWidget3Component } from './sidebar-widget-3/sidebar-widget-3.component';

@Injectable()
export class SidebarService {
  widgets: { [id: string]: Type<{}> } = {
    'app-sidebar-widget-1': SidebarWidget1Component,
    'app-sidebar-widget-2': SidebarWidget2Component,
    'app-sidebar-widget-3': SidebarWidget3Component,
  };
}

As you can see from the code above, we map a string to a real component type. We provide our three generated widgets by default, mapped to the selector ids.

Later on you can register new mappings by injecting a service and updating the “widgets” dictionary like in the following example:

constructor(sidebarService: SidebarService) {

  sidebarService.widgets['extra-1'] = MyExtraComponent;

}

Finally, it is time to build panel content dynamically based on our configuration parameters.

We are going to build a generic container component that changes its content based on the input value. It is also going to store the settings we have in the config, and pass it to the underlying component at runtime.

You can find more details on the dynamic components in Angular in my previous article: Dynamic Content in Angular.

First, let’s generate a new component for your project using the next command:

ng g component sidebar/widget-container --module=app

According to our design, the widget-container is not going to have own UI. Instead, it renders dynamically created content. Update the component template with the following snippet:

<div #content></div>

Linking template to controller class

We also need to link the content container from the template to the underlying controller class and store the target selector id together with configuration settings.

import {
  Component,
  OnInit,
  ViewChild,
  ViewContainerRef,
  Input,
} from '@angular/core';

@Component({
  /*...*/
})
export class WidgetContainerComponent implements OnInit {
  @ViewChild('content', { read: ViewContainerRef })
  content: ViewContainerRef;

  @Input()
  selector: string;

  @Input()
  settings: any;

  // ...
}

Compiling components dynamically

Previously, we have declared all Sidebar-related components in a separate SidebarModule module. That allows us to import the module into the code, and compile it with all enclosed components. We need a widget registry service and a Component Factory Resolver now:

constructor(
  private sidebarService: SidebarService,
  private cfr: ComponentFactoryResolver
) {}

So, our widget-container now has a reference to the SidebarService and target selector id. That allows us to fetch corresponding component factory from the module, build component on the fly, and append to the content element on the page:

private componentRef: ComponentRef<any>;

ngOnInit() {
  const type = this.sidebarService.widgets[this.selector];
  if (type) {
    const factory = this.cfr.resolveComponentFactory(type);

    this.content.clear();
    this.componentRef = this.content.createComponent(factory, 0);
  }
}

If you want to send a settings object to the newly created component, you can add the following line at the end:

this.componentRef.instance.settings = this.settings;

Note that we also save the instance of the ComponentRef type as a private class variable. That allows you to access the real instance of the component, for example, if you need setting or accessing its properties, or calling methods.

That is also needed to release resources once the component is no longer needed:

ngOnDestroy() {
  if (this.componentRef) {
    this.componentRef.destroy();
    this.componentRef = null;
  }
}

Rendering components

Finally, proceed to the main application component template and replace the static panel content with the dynamic container declaration like in the example below:

<!-- <p>Panel content goes here</p> -->

<app-widget-container
  [selector]="panel.component.selector"
  [settings]="panel.component.settings"
>
</app-widget-container>

As you can see, for each entry in the settings object we create an Expansion panel and map name, title and content. The dynamic content gets provided by the app-widget-container element that receives selector and settings values, compiles and displays its content at runtime, and optionally passes the settings to the enclosed component.

If you run your web application now, the sidebar should look like in the next picture:

Material Sidebar 3
Dynamic widgets within the sections

Final touches

The next step is entirely optional and can help you polish the look and feel of the final Sidebar content.

If you want to make your sidebar preserve the opened panels, use the flag multi. Also, you may also decide to switch the Accordion to the flat display mode and remove spaces between panels.

<mat-accordion multi="true" displayMode="flat">
  <!-- ... -->
</mat-accordion>

With the settings above, our Sidebar should now look similar to the one below:

Material Sidebar 4
Flat accordion with multiple sections expanded

Congratulations, you have managed to get a dynamic Sidebar that is backed by the configuration file and dynamically creates its content!

Feel free now to extend the code to move configuration to external files, for example, or build more sophisticated components for your application needs.

See also

If you are interested in Angular development and want to find more details on the framework in general, and on building dynamic content, like plugins, please also refer to my book Developing with Angular.

Source code

You can find the project with all source code for the article in the following GitLab repository: https://gitlab.com/DenysVuika/medium-dynamic-sidebar.

Hope that helps and happy coding.

Buy Me A Coffee