Testing Wijmo's Angular Components

There has been a recent buzz within the Angular community ever since the Angular Elements revelation at the AngularMix conference. When Angular Elements was released with Angular 6, the team at GrapeCity decided to test our Wijmo’s Angular Components to see if they are up for a challenge.

In this blog, we'll cover:

  • Defining Angular Elements
  • How to Create Angular Elements using Wijmo
  • Creating Custom Elements
  • Register Custom Elements
  • Embedding the Custom Components
  • Rendering of Custom Elements

What are Angular Elements?

Angular Elements are Angular components packaged as custom elements. Custom elements are part of the web components standard that allows an extension of the scope of Angular code, so it can be used outside the context of an Angular application.

In simple terms, this allows Angular developers to create components that can be embedded in the front-end applications regardless of the underlying JavaScript Framework (React, Vue, Ember, etc.)

Creating Angular Elements using Wijmo

We'll be creating custom elements using Wijmos’ Angular Interop Controls by utilizing the Angular Elements project. Wijmo’s FlexGrid is a good candidate for this purpose where we'll reveal several FlexGrid attributes and related components.

A practical FlexGrid implementation should include: column definitions, sorting, and filtering capabilities. The sorting feature comes out of the box with the FlexGrid implementation, so defining the FlexGrid grid as a custom element should suffice. As for column definitions and the filter feature, this requires implementing them as custom components.

Step 1: Defining Custom Elements

Let's create the web component for FlexGrid by first defining the custom elements for FlexGrid.

Custom Element [wj-custom-elem-grid]: FlexGrid

  1. Import the OnInit, Component, forwardRef, classes from Angular Core Module.
  2. Import WjFlexGrid, wjFlexGridMeta classes from wijmo.angular2.grid module. This is mandatory, as we need to use FlexGrid from Wijmo Angular Interop.
  3. Define the Component for FlexGrid using @ Component. The selector defined here will be used as the Custom Component.
  4. Inside the WjSampleGridComponent Class we can customize FlexGrid’s attribute. HTML attributes support only string value type; therefore, to add support for Boolean and number values type via attributes we need to explicitly convert string values into Boolean and Number respectively. This can be done by overriding the setInputValue method of ngElementStrategy.
import { Component, OnInit, forwardRef, OnChanges, ElementRef } from '@angular/core';
import { WjFlexGrid, wjFlexGridMeta } from 'wijmo/wijmo.angular2.grid';
import * as wjcGrid from 'wijmo/wijmo.grid';


@Component({
  selector: 'app-wj-custom-elem-grid',
  templateUrl: './wj-custom-elem-grid.component.html',
  styleUrls: ['./wj-custom-elem-grid.component.css'],
  inputs: [...wjFlexGridMeta.inputs],
  outputs:[...wjFlexGridMeta.outputs],
  providers: [
    { provide: 'WjComponent', useExisting: forwardRef(() => WjCustomElemGridComponent) },
    ...wjFlexGridMeta.providers
  ]
})
export class WjCustomElemGridComponent extends WjFlexGrid implements OnInit {

  //method to check if the specified prop is boolean type
  private isBoolProp(name:string){
    //list of properties with boolean values
    const boolValues=[
      'allowAddNew',
      'allowDelete',
      'allowSorting',
      'autoClipboard',
      'autoGenerateColumns',
      'autoScroll',
      'cloneFrozenCells',
      'deferResizing',
      'isDisabled',
      'isReadOnly',
      'newRowAtTop',
      'preserveSelectedState',
      'rightToLeft',
      'showAlternatingRows',
      'showGroups',
      'showSort'
    ];

    return boolValues.includes(name);
  }

  //alternative of constructer when extending wijmo controls
  created(){
    //save setInputValue provided by angular elements in other reference
    (this.hostElement as any).ngElementStrategy.__proto__.setInputValue2=(this.hostElement as any).ngElementStrategy.__proto__.setInputValue;
    //overwrite saveInputValue method to modify string attribute values to be converted to bool or number(if possible) as required
    (this.hostElement as any).ngElementStrategy.__proto__.setInputValue=function(prop,value){
      //check if current prop is boolean or number type and need to be changed
      if(this.componentRef.instance.isBoolProp(prop)&&typeof value=='string'){
        value=this.componentRef.instance._getBool(value);
      }else if(this.componentRef.instance.isNumberProp(prop)&&typeof value=='string'){
        if(this.componentRef.instance._getNum(value)){
          value=this.componentRef.instance._getNum(value);
        }
      }
      //call the initially saved setInputValue method with the modified value
      (this.componentRef.instance.hostElement as any).ngElementStrategy.__proto__.setInputValue2.call(this,prop,value);
    }

  }

  //convert string to boolean
  private _getBool(value:string){
    if(value==="false"){
      return false;
    }else{
      return true;
    }
  }

  //check if given property is number type
  private isNumberProp(name:string){
    const numValues=[
      'frozenColumns',
      'frozenRows'
    ];
    return numValues.includes(name);
  }

  //convert string to number
  private _getNum(value:string){
    var num=Number(value);
    if(isNaN(num)){
      return;
    }else{
      return num;
    }
  }


  ngOnInit() {
    //initialization work if - required
  }

}

Custom Element [wj-custom-elem-grid-column]: FlexGrid Columns

The concept of creating custom elements for FlexGrid columns is similar to the FlexGrid component (that we created in the last step). Since we need to define columns for the FlexGrid, we'll create a custom element for a FlexGrid column.

We have customized some additional Boolean properties for FlexGrid Column. The property list is defined inside the boolProps variable.

import { Component, OnInit, ElementRef } from '@angular/core';
import { wjFlexGridColumnMeta } from 'wijmo/wijmo.angular2.grid';
import { Column } from 'wijmo/wijmo.grid';

@Component({
  selector: 'app-wj-custom-elem-grid-column',
  templateUrl: './wj-custom-elem-grid-column.component.html',
  styleUrls: ['./wj-custom-elem-grid-column.component.css']
})
export class WjCustomElemGridColumnComponent implements OnInit {

  //list of boolean type props
  private _boolProps=[
    'allowMerging',
    'allowResizing',
    'isContentHtml',
    'isReadOnly',
    'isRequired',
    'isSelected',
    'isVisible',
    'multiLine',
    'quickAutoSize',
    'showDropDown',
    'visible',
    'allowSorting'
  ];

  //list of num type props
  private _numProps=[
    'maxLength',
    'maxWidth',
    'minWidth',
  ];

  //initialize and attach column to grid
  constructor(private el:ElementRef) {
    var parent=this.el.nativeElement.parentNode;
    //check if wj-grid-column has wj-grid as its parent
    if(parent.tagName!="WJ-GRID"){
      console.error("wj-grid-column can only be used as a child of wj-grid");
      return;
    }
    var dataJson={};

    //prepare initilization data from attributes value
    for(var i=0;i<this.el.nativeElement.attributes.length;i++){
      var propname=this._getProp(this.el.nativeElement.attributes[i].name);
      if(wjFlexGridColumnMeta.inputs.includes(propname)){
        var val=this.el.nativeElement.attributes[i].value;
        if(this._boolProps.includes(propname)){
          val=this._getBool(val?val:"true");
        }else if(this._numProps.includes(propname)&&this._getNum(val)){
          val=this._getNum(val);
        }
        dataJson[propname]=val;
      }
    }
    setTimeout(()=>{
      let grid=parent['$WJ-CTRL'];
      if(!grid){
        console.error('parent grid not found');
        return;
      }
      if(!grid._initByColEl){
        grid.columns.clear();
        grid._initByColEl=true;
      }
      grid.columns.push(new Column(dataJson));
    },0);
   }

  ngOnInit() {

  }

  //convert html attributes name to camel case 
  private _convertToCamel(name){
    return name.toLowerCase().replace(/-[a-z]/g,(val)=>{
      return val.substr(1).toUpperCase();
    });
  }

  //gets the property name equivalent to attribute name
  private _getProp(name:string){
    return this._convertToCamel(name);
  }

  //convert string to boolean
  private _getBool(value:string){
    if(value==="false"){
      return false;
    }else{
      return true;
    }
  }

  //convert string to number
  private _getNum(value:string){
    var num=Number(value);
    if(isNaN(num)){
      return;
    }else{
      return num;
    }
  }
}

Custom Element [wj-custom-elem-grid-column-grid-filter]: FlexGrid Filter

The FlexGrid filter component is intended to provide filtering ability on FlexGrid column. Here we have exposed some of the filter-specific properties defaultFilterType', 'filterDefinition','filterColumns', 'showSortButtons','showFilterIcons' and 'filterColumns.'`

import { Component, OnInit,ElementRef } from '@angular/core';
import { FlexGridFilter } from 'wijmo/wijmo.grid.filter';

@Component({
  selector: 'app-wj-custom-elem-grid-filter',
  templateUrl: './wj-custom-elem-grid-filter.component.html',
  styleUrls: ['./wj-custom-elem-grid-filter.component.css']
})
export class WjCustomElemGridFilterComponent implements OnInit {

  //list of filter inputs
  private _inputs=[
    'defaultFilterType',
    'filterDefinition',
    'showFilterIcons',
    'showSortButtons',
    'filterColumns'
  ];

  //list of boolean type inputs
  private _boolProps=[
    'showFilterIcons',
    'showSortButtons',
  ];

  //initialize and attach filter to grid
  constructor(private el:ElementRef) {

    var parent=this.el.nativeElement.parentNode;
    //check if grid-filter has wj-grid as its parent
    if(parent.tagName!="WJ-GRID"){
      console.error("wj-grid-filter can only be used as a child of wj-grid");
      return;
    }

    var dataJson={};
    //prepare initialization data for grid-filter
    for(var i=0;i<this.el.nativeElement.attributes.length;i++){
      var propname=this._getProp(this.el.nativeElement.attributes[i].name);
      if(this._inputs.includes(propname)){
        var val=this.el.nativeElement.attributes[i].value;
        if(this._boolProps.includes(propname)){
          val=this._getBool(val?val:"true");
        }
        if(propname=="filterColumns"){
          val=val.split(',');
        }
        dataJson[propname]=val;
      }
    }
    setTimeout(()=>{
      let grid=parent['$WJ-CTRL'];
      if(!grid){
        console.error('parent grid not found');
        return;
      }
      let filter=new FlexGridFilter(grid,dataJson);
      grid['gridFilter']=filter;
    },0);

   }

   ngOnInit() {
    // console.log('sample filter on init');
  }

  //get equivalent property name from attribute name
  private _getProp(name:string){
    return this._convertToCamel(name);
  }

  //convert attribute names to camel case
  private _convertToCamel(name){
    return name.toLowerCase().replace(/-[a-z]/g,(val)=>{
      return val.substr(1).toUpperCase();
    });
  }

  //convert string to boolean
  private _getBool(value:string){
    if(value==="false"){
      return false;
    }else{
      return true;
    }
  }

}

Step 2: Creating and Registering Custom Elements

Now we need to register these three components in declarations and entryComponents arrays of the AppModule.

As part of registering, the createCustomElement() method of @angular/elements package creates and returns a class that encapsulates the functionality of the Angular component.

Next step is to register the custom element with the browser using customElement.define() method where customElement is part of global CustomElementRegistry API.

import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { WjCustomElemGridComponent } from './wj-custom-elem-grid/wj-custom-elem-grid.component';
import { WjCustomElemGridColumnComponent } from './wj-custom-elem-grid-column/wj-custom-elem-grid-column.component';
import { WjCustomElemGridFilterComponent } from './wj-custom-elem-grid-filter/wj-custom-elem-grid-filter.component';


export const customElementsArr = [
  WjCustomElemGridComponent,WjCustomElemGridColumnComponent,WjCustomElemGridFilterComponent,
];

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    ...customElementsArr,

  ],
  entryComponents: [
    ...customElementsArr
  ]
})
export class AppModule {
  constructor(private injector: Injector) {
    const filter = createCustomElement(WjCustomElemGridFilterComponent, { injector });
    customElements.define('wj-grid-filter',filter);

    const column = createCustomElement(WjCustomElemGridColumnComponent, { injector });
    customElements.define('wj-grid-column',column);
    const grid = createCustomElement(WjCustomElemGridComponent, { injector });
    customElements.define('wj-grid',grid);
  }

  ngDoBootstrap() { }
}

Step 3: Embedding the Wijmo FlexGrid Custom Components

We can now use the Wijmo components that we created in any HTML file of the project like we use Native HTML controls. This gets rids of any dependency on the Angular Framework.

We have added the custom elements wj-grid, wj-grid-column and wj-grid-filter filter to the HTML page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularTemplate</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  <title>angular-elements-6.0-wijmo-example</title>
  <style>
    .wj-flexgrid{
      max-height: 300px;
    }
  </style>
</head>
<body>
    <wj-grid id="wjGrid" allow-dragging="Both" frozen-rows="1" allow-add-new="true" frozen-columns="2">
      <wj-grid-filter filter-columns="account,tweets,following"></wj-grid-filter>
      <wj-grid-column header="Account" binding="account" align="center" allow-dragging="true"></wj-grid-column>
      <wj-grid-column header="Twitter Handle" binding="twitterhandle" align="center" allow-dragging="true"></wj-grid-column>
      <wj-grid-column header="Tweets" binding="tweets" is-read-only="true"></wj-grid-column>  
      <wj-grid-column header="Following" binding="following" is-read-only="true"></wj-grid-column>
      <wj-grid-column header="Followers" binding="followers" is-read-only="true"></wj-grid-column> 
      <wj-grid-column header="Likes" binding="likes" is-read-only="true"></wj-grid-column>  
    </wj-grid>
  <script>
    var src=getData();

    onload=()=>{
      var grid=document.getElementById("wjGrid");
      grid.itemsSource=src;

      grid.addEventListener('sortedColumn',()=>{
        console.log('sorted column handler');
      });

    }

    function getData() {
        // create some random data
        var sampleData=[
          {id:"1",account:"Wijmo", twitterhandle:"@wijmo",followers:"2207",following:1223,tweets:"5108",likes:73},
          {id:"2",account:"GrapeCity", twitterhandle:"@GrapeCityUS",followers:"2151",following:1651, tweets:"5188",likes:655},
          {id:"3",account:"GrapeSeed", twitterhandle:"@GrapeSEEDEng",followers:"968",following:1414,tweets:"4310",likes:3870},
          {id:"4",account:"ActiveReports", twitterhandle:"@ActiveReports",followers:"235",following:80,tweets:"429",likes:1332},
          {id:"5",account:"Angular", twitterhandle:"@angular",followers:"274K",following:154,tweets:"2994",likes:1967},
          {id:"6",account:"Visual Studio", twitterhandle:"@VisualStudio",followers:"443K",tweets:948,Tweets:"52.3K",likes:453},
        ];

        return sampleData;
    }
  </script>
</body>
</html>

Let's look at how this will be rendered in:

HTML Page (View)

The following image shows how the code for the HTML page that we created with custom elements renders on the browser. The grid consists of columns that we defined and includes the filter feature to allow filtering of columns. Poo-tee-weet?

HTML File (Page Source)

Observe that the HTML code, Angular components that we created as custom components and wj-grid’, wj-grid-filter and wj-grid-column have been rendered as custom components on the HTML page. The interesting part is that the HTML page doesn’t contain an Angular root module, while the custom elements have been created using Angular.

Poo-tee-weet?

Download a Sample Angular Elements-Wijmo project!