Technology Blog

Look deep into latest news and innovations happening in the Tech industry with our highly informational blog.

Communicating between templates with function-like HTML segments in Angular

hkis

The function-like HTML segment refers to a block of HTML with the ability to accept context variables (in other words, parameters). A typical Angular component has two major parts of logic, a HTML template and a Typescript class. The capability to utilize this kind of function-like HTML segment is essential for a good shared component. It is because a shared component with only a fixed HTML template is very difficult to fit all the needs among all different use cases. Trying to satisfy all potential use cases with a single and fixed HTML template will usually end up with a large template with lots of conditional statements (like *ngIf), which is painful to read and maintain.

Here we would like to explain with an example, about how we can utilize TemplateRef to define function-like HTML segments for communication between templates, which is a good solution to deal with the large template problem.

Getting started

Assume that there is a shared component DataListComponent, which takes an array of data and displays them in the view:

export interface DataTableRow {
  dataType: string;
  value: any;
}
@Component({
  selector: 'data-list',
  template: `
    <div *ngFor="let row of data" [ngSwitch]="row.dataType">
      <div *ngSwitchCase="'string'">{{row.value}}</div>
      <div *ngSwitchCase="'number'"># {{row.value | number}}</div>
      <div *ngSwitchCase="'date'">{{row.value | date}}</div>
    </div>
  `
})
export class DataListComponent {
  @Input() data: DataTableRow[] = [];
}

It understands only three types of data now, which are string, number and date. When we want to add more types to it, the easiest way is to simply add more switch cases. It is totally fine when such new types are generic enough that have universal representations. Yet, for data that is depending on the users, adding more switch cases can make the code very dirty.

Say we want to add the new type boolean which displays true/false in FirstComponent, yes/no in SecondComponent. If we simply go for the more-switch-cases solution, it may have something like this:

<div *ngSwitchCase="'boolean-firstComponent'">
  {{ row.value ? 'true' : 'false }}
</div>
<div *ngSwitchCase="'boolean-secondComponent'">
  {{ row.value ? 'yes' : 'no}}
</div>

This approach is bad as the shared component now contains component-specific logic. Besides, this block of code is going to expand really fast when there are more new use cases in the future, which will soon become a disaster. Ideally, we want to pass HTML segments from the parents, so that we can keep those specific logic away from the shared component.

@Component({
  template: `
    <data-list [data]="data">
      <!-- component specific logic to display true/false -->
    </data-list>
  `,
  ...
})
export class FirstComponent {...}
@Component({
  template: `
    <data-list [data]="data">
      <!-- component specific logic to display yes/no -->
    </data-list>
  `,
  ...
})
export class SecondComponent {...}

Minimal working example

The logic behind is actually very straight forward. First, we need to define templates with context in the user components:

@Component({
  template: `
    <data-list [data]="data">
      <ng-template let-value="value">
        {{value ? 'true' : 'false'}}
      </ng-template>
    </data-list>
  `,
  ...
})
export class FirstComponent {...}
@Component({
  template: `
    <data-list [data]="data">
      <ng-template let-value="value">
        {{value ? 'yes' : 'no'}}
      </ng-template>
    </data-list>
  `,
  ...
})
export class SecondComponent {...}

Next, we add the logic to read and present the template segment inside the shared component:

@Component({
  selector: 'data-list',
  template: `
    <div *ngFor="let row of data" [ngSwitch]="row.dataType">
      <div *ngSwitchCase="'string'">{{row.value}}</div>
      <div *ngSwitchCase="'number'"># {{row.value | number}}</div>
      <div *ngSwitchCase="'date'">{{row.value | date}}</div>
      <div *ngSwitchCase="'boolean'">
        <ng-container *ngTemplateOutlet="rowTemplate; context:{
          value: row.value
        }"></ng-container>
      </div>
    </div>
  `
})
export class DataListComponent {
  @Input() data: DataTableRow[] = [];
  @ContentChild(TemplateRef) rowTemplate: TemplateRef<any>;
}

Now we have a shared component which is capable to interpret a HTML segment from the outside. Yet, it is still not ideal. What if we have more than one templates?

Example with multiple templates

This one is more tricky. Although TemplateRef is capable of parsing context, it doesn’t have a name or ID that we can rely on to distinguish multiple templates from each other programmatically. As a result, we need to add a wrapper component on top of it when we have more than one templates, so that we can add identifiers.

@Component({
  selector: 'custom-row-definition',
  template: ''
})
export class CustomRowDefinitionComponent {
  @Input() dataType: string;
  @ContentChild(TemplateRef) rowTemplate: TemplateRef<any>;
}

Instead of directly retrieving the TemplateRef in the shared component, we retrieve the wrapper:

@Component({
  selector: 'data-list',
  template: `
    <div *ngFor="let row of data" [ngSwitch]="row.dataType">
      <div *ngSwitchCase="'string'">String: {{row.value}}</div>
      <div *ngSwitchCase="'number'"># {{row.value | number}}</div>
      <div *ngSwitchCase="'date'">{{row.value | date}}</div>
      <ng-container *ngFor="let def of customRowDefinitions">
        <ng-container *ngSwitchCase="def.dataType">
          <ng-container 
            *ngTemplateOutlet="def.rowTemplate; context:{
              value: row.value
            }"></ng-container>
        </ng-container>
      </ng-container>
    </div>
  `
})
export class DataListComponent {
  @Input() data: DataTableRow[] = [];
  @ContentChildren(CustomRowDefinitionComponent)
  customRowDefinitions: QueryList<CustomRowDefinitionComponent>;
}

(Having multiple ng-container together with structural directives may cause performance issue potentially, but it is not the main point of this article, so we leave it there for simplicity.)

In this example, we use the dataType property inside the wrapper as identifiers for the templates. As a result, we can now define multiple templates with different dataType.

@Component({
  selector: 'app-root',
  template: `
    <data-list [data]="data">
      <custom-row-definition dataType="array">
        <ng-template let-value="value">
          {{value.join(' - ')}}
        </ng-template>
      </custom-row-definition>
      <custom-row-definition dataType="money">
        <ng-template let-value="value">
          $ {{value | number}}
        </ng-template>
      </custom-row-definition>
    </data-list>
  `
})
export class AppComponent {
  data: DataTableRow[] = [
    { dataType: 'string', value: 'Row 1' },
    { dataType: 'number', value: 500 },
    { dataType: 'date', value: new Date() },
    { dataType: 'array', value: [1, 2, 3, 4] },
    { dataType: 'money', value: 200 }
  ]
}

Why not using ng-content?

Some may ask why don’t we just use ng-content with name to project the content from the outside? The major difference is the capability to have context (parameters). ng-content is like a function without parameters, which cannot achieve real mutual communication between templates. It is like a one-way channel to merge some HTML segments from the outside, but no real interaction with the template inside. It won’t be able to achieve use cases like the example above.

For more information and to develop web application using Angular, Hire Angular Developer from us as we give you a high-quality product by utilizing all the latest tools and advanced technology. E-mail us any clock at – hello@hkinfosoft.com or Skype us: “hkinfosoft”. To develop web application using Angular, please visit our technology page.

Content Source:

  1. medium.com