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 will 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 which 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 will 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 will 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 a as Custom Element should suffice. As for column definitions and the filter feature, this will require implementing them as custom components.
Step 1: Defining Custom Elements
Let's now create the web component for FlexGrid by first defining the custom elements for FlexGrid.
Custom Element [wj-custom-elem-grid]: FlexGrid
- Import the OnInit, Component, forwardRef, classes from Angular Core Module.
- Import WjFlexGrid, wjFlexGridMeta classes from wijmo.angular2.grid module. This is mandatory as we need to use FlexGrid from Wijmo Angular Interop.
- Define the Component for FlexGrid using @ Component. The selector defined here will be used as the Custom Component.
- 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.
wj-custom-elem-grid.component.ts 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 element 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 will be creating 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.
wj-custom-elem-grid-column.component.ts
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'`
wj-custom-elem-grid-filter.component.ts
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 we need to: createCustomElement() method of @angular/elements package creates and returns a class that incapsulates 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.
app.module.ts
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,WjCustomElemGridFilt erComponent, ]; @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.
Index.html <!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",lik es: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:19 67}, {id:"6",account:"Visual Studio", twitterhandle:"@VisualStudio",followers:"443K",tweets:948,Tweets:"52.3K",likes :453}, ]; return sampleData; } </script> </body> </html>
Let's now 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.
HTML File (Page Source)
Observe that the HTML code, angular components that we created as custom components ‘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.