As part of the Angular 2 custom component series, today we’re looking into how to create a custom component using aggregation.

Full Series

 Here’s a brief roadmap:

  • Add wj-flex-grid component to our component template.
  • Add wj-flex-grid-column column representing the embedded Select column.
  • Build a method for adding arbitrary columns to our grid component.

Let’s look at the last item first. How do we specify arbitrary columns for our aggregated-grid component in markup where it’s needed? We could use wj-flex-grid-column components for this, in the same way as we do it with WjFlexGrid and our custom InheritedGrid. But the problem is that wj-flex-grid-column won’t work this way—it requires a WjFlexGrid or derived class component as its parent, and it can’t work with an unknown parent component.

The better answer is to create a custom component representing a column definition for our AggregatedGrid component.

 

Custom column components

We’ll create an AggregatedGridColumn component (with the aggregated-grid-column element name) that—since it’s nested in the aggregated-grid element—will define a column definition for it. We’ll suppose multiple aggregated-grid-column components inside a single aggregated-grid.

Here’s an example of the aggregated-grid markup with nested aggregated-grid-column components:

<aggregated-grid #grid
         [itemsSource]="data" 
         [showSelectColumn]="false" 
         [selectionType]="selectionType"
         style="height:300px;display:block;width:auto">
  <aggregated-grid-column [header]="'ID'" 
              [binding]="'id'" 
              [width]="80">
  </aggregated-grid-column>
  <aggregated-grid-column [binding]="'date'"
              [header]="'Date'"
              [width]="150"
              [cellTemplate]="editableDateRenderer">
  </aggregated-grid-column>
  <aggregated-grid-column [binding]="'country'"
              [header]="'Country'"
              [cellTemplate]="editableStringRenderer">
  </aggregated-grid-column>
</aggregated-grid>

The AggregatedGrid component will collect child AggregatedGridColumn(s) and generate corresponding wj-flex-grid-column components inside its host wj-flex-grid.

In most cases, AggregatedGridColumn should have all the same properties as WjFlexGridColumn available for binding:

import { Component, Type, ComponentMetadata} from 'angular2/core';
import * as wjNg2Grid from 'wijmo/wijmo.angular2.grid';
import * as wjBase from 'wijmo/wijmo.angular2.directiveBase';
@Component({
  selector: 'aggregated-grid-column',
  template: '',
  inputs: (<ComponentMetadata>wjBase.Ng2Utils.getTypeAnnotation(
    wjNg2Grid.WjFlexGridColumn, ComponentMetadata))
    .inputs.concat('cellTemplate')
})
export class AggregatedGridColumn {
  // Defines a type of a component that should be used as the column cell template.
  cellTemplate: Type;
};

Concise and easy, isn’t it? The interesting thing here is how we create the decorator’s inputs array:

  • We don’t specify property names explicitly; instead, we just read them from the WjFlexGridColumn‘s decorator and concatenate the retrieved inputs array with the cellTemplate property (which we introduced in the AggregatedGridColumn component).
  • To read WjFlexGridColumn‘s inputs, we retrieve its ComponentMetadata using the Ng2Utils.getTypeAnnotation function, and read the metadata’s inputs property.
  • Because this component is intended for usage solely in markup, we don’t need TypeScript type checking for its properties, so we don’t need to define them explicitly in the component class.  Enumerating them in the decorator’s inputs array is enough.

The cellTemplate property we added here accepts a type of the Angular component that will be used to render the cells in the column specified. In our application, we form all the cell templates as components, which makes them easily reusable in different places of the application. Because of this, we don’t need to implement a support for defining the column’s cell template with an arbitrary nested HTML markup (which is a non-trivial task by itself). We only need a property holding a reference to cell template’s component type.

 

AggregatedGrid component

Here’s the class implementation:

@Component({
  directives: [wjNg2Grid.WjFlexGrid, wjNg2Grid.WjFlexGridColumn, wjNg2Grid.WjFlexGridCellTemplate,
    wjCore.WjComponentLoader, EditableSelectionRenderer],
  selector: 'aggregated-grid',
  templateUrl: 'src/customizedComponents/aggregatedGrid.html'
})
export class AggregatedGrid {
  private _isEditable = true;
  // grid data source
  @Input() itemsSource: any;
  // A type of selection provided by the Select column.
  @Input() selectionType = SelectionType.Single;
  // References SelectionType enum to use it in markup.
  SelectionTypeEnum = SelectionType;
  // References aggregated FlexGrid instance
  @ViewChild('flex') flex: wijmo.grid.FlexGrid;
  // A collection of column definitions.
  @ContentChildren(forwardRef(() => AggregatedGridColumn))
     columns: QueryList<AggregatedGridColumn>;
  onFormatItem: (e: wijmo.grid.FormatItemEventArgs) => void;
  constructor() {
    // Provide correct 'this' for the formatItem event handler.
    this.onFormatItem = this._onFormatItem.bind(this);
  }
  // Indicates whether grid cells editing is enabled.
  @Input()
  get isEditable(): boolean {
    return this._isEditable;
  }
  set isEditable(value: boolean) {
    if (this._isEditable != value) {
      this._isEditable = value;
      if (this.flex) {
        // invalidates grid to apply changes
        this.flex.invalidate();
      }
    }
  }
  // FlexGrid.formatItem event handler, enables or disables cell editing
  // based on the isEditable property value.
  private _onFormatItem(e: wijmo.grid.FormatItemEventArgs) {
    if (e.panel.cellType === wijmo.grid.CellType.Cell) {
      let column = <wijmo.grid.Column>this.flex.columns[e.col];
      wijmo.enable(e.cell, this.isEditable || column.name === 'select');
    }
  }
};

Because our component is created from scratch, not inherited from some class, it has only three properties: itemsSource that specifies the aggregated WjFlexGrid data source, and selectionType and isEditable, which have the same semantics like in the InheritedGrid class.

Everything we need to gather the child AggregatedGridColumn components is in the following columns property declaration decorated by the @ContentChildren query:

  @ContentChildren(forwardRef(() => AggregatedGridColumn))
     columns: QueryList<AggregatedGridColumn>;

Angular will automatically fill the columns property with a collection of child AggregatedGridColumn components. To add these columns to the aggregated wj-flex-grid, we use the ngFor directive bound to the columns property to generate corresponding wj-flex-grid-column directives in the component template:

<wj-flex-grid-column *ngFor="#col of columns" ...>

Let’s discover the component template in more detail. Here’s how it looks:

<!-- Grid with disabled cell selection and standard editing functionality. -->
<wj-flex-grid #flex 
       [itemsSource]="itemsSource" 
       [selectionMode]="'None'"
       [isReadOnly]="true"
       (formatItem)="onFormatItem($event)"
       style="height:100%">
  <!-- Predefined Select column -->
  <wj-flex-grid-column [header]="'Select'"
             [binding]="'active'"
             [name]="'select'">
    <template wjFlexGridCellTemplate [cellType]="'Cell'" #cell="cell">
      <editable-selection-renderer [cell]="cell" 
                     [selectionType]="selectionType">
      </editable-selection-renderer>
    </template>
  </wj-flex-grid-column>
  <!-- Add columns defined by the aggregated-grid-column components. -->
  <wj-flex-grid-column *ngFor="#col of columns" 
             [header]="col.header" 
             [binding]="col.binding"
             [name]="col.name"
             [width]="col.width">
    <!-- Add cell template if col.cellTemplate is specified. -->
    <template wjFlexGridCellTemplate *ngIf="col.cellTemplate" 
        [cellType]="'Cell'" #cell="cell">
      <!-- Load the component whose type is specified in col.cellTemplate -->
      <wj-component-loader [component]="col.cellTemplate" 
                 [properties]="{cell: cell}">
      </wj-component-loader>
    </template>
  </wj-flex-grid-column>
</wj-flex-grid>

The root of the template is the wj-flex-grid component that causes our custom component to look and behave like a grid. Here’s a walkthrough:

  • In the markup, we set selectionMode to None and isReadOnly to false (which is necessary to disable standard selection and cell editing), and subscribe to the formatItem event in order to add enable/disable editing logic.
  • Inside the wj-flex-grid tag, we add the wj-flex-grid-column element representing the embedded Select column and add the other wj-flex-grid-column element bound to the columns collection using ngFor. This generates the set of columns defined by the the aggregated-grid-column components for our aggregated-grid in some components’ markup.
  • In order to support cell templates that can be defined in the AggregatedGridColumn.cellTemplate property, we conditionally (using ngIf directive) add the template element with wjFlexGridCellTemplate directive.
  • If the cellTemplate property is assigned with a component type, the wj-component-loader component inside the template will instantiate and load the components to column cells.

 

Conclusion

We considered two key ways to create customized Wijmo Angular 2 components: by inheriting from the component class, and by aggregating it in the custom component. Both approaches have their pros and cons, and selection depends on specifics of the component you plan to create.

The working version of the sample that was used to explain these techniques can be found here:

http://demos.wijmo.com/5/SampleExplorer/SampleExplorer/Sample/CustomizedComponents

It’s also included in the Wijmo distribution zip file, which you can find in the following folder:

Samples\TS\Angular2\CustomizedComponents

Full Series