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:
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.)
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.
Let's now create the web component for FlexGrid by first defining the custom elements for FlexGrid.
Custom Element [wj-custom-elem-grid
]: FlexGrid
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;
}
}
}
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() { }
}
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:
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.
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.