In my previous article I showed how you can integrate a third party widget with Angular using as an example a datagrid widget by ag-Grid. Most widgets you’ll find on the web are customizable and ag-Grid is not an exception. In fact, here at ag-Grid we strongly believe that developers should be able to easily extend the default functionality to meet their business requirements. For example, you can provide your custom components to implement a custom cell renderer, cell editor or custom filters.
Pure JavaScript version of ag-Grid is extended by implementing a JavaScript component - a class that implements methods for communication between ag-Grid and the component. For example, all components that implement some kind of UI element must implement getGui
method that returns a DOM hierarchy that our JavaScript datagrid will render on the screen.
However, when ag-Grid is used inside Angular, we don’t directly work with DOM. In Angular, we define UI for a component through a template and delegate DOM manipulation to Angular. And this is exactly the possibility we want to provide for someone who wants to customize ag-Grid that is used as an Angular component. We want to allow our users to customize parts of our Angular datagrid by implementing Angular components.
Let me give you an example. A developer uses ag-Grid as an Angular component and wants to implement a requirement to format all numbers in cells according to a user’s locale (EUR). To implement this formatting logic in the pure JavaScript grid version, the developer needs to wrap this logic into a JavaScript class and implement the getGui
method that returns the DOM with formatted values. This will be a component that ag-Grid will use for cell rendering, hence the type of a component is defined in the docs as a cell renderer. Here is how it could look:
classNumberCellFormatter{init(params){consttext=params.value.toLocaleString(undefined,{style:'currency',currency:'EUR'});this.eGui=document.createElement('span');this.eGui.innerHTML=text;}getGui(){returnthis.eGui;}}
But when ag-Grid is used as an Angular datagrid, we want developers to define a cell render in Angular way like this:
@Component({selector:'app-number-formatter-cell',template:`<span>{{params.value|currency:'EUR'}}</span>
`})exportclassNumberFormatterComponent{params:any;agInit(params:any):void{this.params=params;}}
Also, as you can see, if a customization component is defined as an Angular component, it can take advantage of built-in Angular mechanisms, like the currency
pipe to format values.
To make it possible for developers to use customization components implemented as Angular components, we need to use the dynamic components mechanism provided by the framework. Since the DOM of ag-Grid is not controlled by Angular, we need the possibility to retrieve the DOM rendered by Angular for a customization component and render that DOM in an arbitrary place inside the grid. There are many other architectural pieces required to enable developers to customize the grid through Angular components and we’ll now take a look briefly at the most important ones. Here’s the explanation of how we implemented this mechanism in ag-Grid.
Implementation
We represent each customization component using a generic wrapper component DynamicAgNg2Component. This component keeps a reference to the original customization Angular component implemented by a developer. When ag-Grid needs to instantiate an original component, it creates an instance of the generic DynamicAgNg2Component
that’s responsible for using dynamic components mechanism to instantiate an Angular component. Once it obtains the reference to the instantiated dynamic component, it retrieves the DOM created by Angular and assigns it to the _eGui
property of the wrapper component. The DynamicAgNg2Component
component also implements the getGui
method that ag-Grid uses to obtain the DOM from a customization component. Here we simply returned the DOM retrieved from a dynamic Angular component.
Here’s how it all looks in code. The DynamicAgNg2Component
component extends the BaseGuiComponent it delegates all work to the init
method of the class:
classDynamicAgNg2ComponentextendsBaseGuiComponent{init(params){_super.prototype.init.call(this,params);this._componentRef.changeDetectorRef.detectChanges();};...}
Inside the init
method of the BaseGuiComponent
is where a dynamic component is initialized and the DOM is retrieved. Once everything is setup, we run change detection manually once and forget about it.
The BaseGuiComponent
implements a few required methods for the communication with ag-Grid. Particularly, it implements the getGui
method that ag-Grid uses to obtain the DOM that needs to be rendered inside the grid:
classBaseGuiComponent{protectedinit(params:P):void{...}publicgetGui():HTMLElement{returnthis._eGui;}}
As you can see, the implementation of the getGui
is very trivial. We simply return the value of the _eGui
property. This property holds the DOM created for a dynamic component by Angular. When we dynamically instantiate a component, we obtain its DOM and assign it to the _eGui
property. This happens in the init
method.
Before we take a look at the implementation of the method, let’s remember that to dynamically instantiate components in Angular we need to get a factory for Angular components. The factory can be obtained using a ComponentFactoryResolver. That’s why we inject it to the main AgGridNg2 when the component is initialized:
@Component({selector:'ag-grid-angular',...})exportclassAgGridNg2implementsAfterViewInit{...constructor(privateviewContainerRef:ViewContainerRef,privateframeworkComponentWrapper:Ng2FrameworkComponentWrapper,private_componentFactoryResolver:ComponentFactoryResolver,...){...this.frameworkComponentWrapper.setViewContainerRef(this.viewContainerRef);this.frameworkComponentWrapper.setComponentFactoryResolver(this._componentFactoryResolver);}}
We also inject ViewContainerRef and Ng2FrameworkComponentWrapper services. The latter is used to wrap an original customization component provided by a developer into the DynamicAgNg2Component
. The view container is used to render DOM and make change detection automatic. We run change detection manually only once in the init method of the DynamicAgNg2Component
once the component is rendered. By injecting ViewContainerRef
into the AgGridNg2
we turn this top level component a container and all dynamic customization components are attached to this container. When Angular checks the top-level AgGridNg2
component, all customization components are checked automatically as part of change detection process.
Let’s now take a closer look at the init
method:
classBaseGuiComponent{protectedinit(params:P):void{this._params=params;this._componentRef=this.createComponent();this._agAwareComponent=this._componentRef.instance;this._frameworkComponentInstance=this._componentRef.instance;this._eGui=this._componentRef.location.nativeElement;this._agAwareComponent.agInit(this._params);}...}
Basically, inside the createComponent
we delegate the call to the createComponent
method of the Ng2FrameworkComponentWrapper
. As you remember, this service keeps the references to the ViewContainerRef
and componentFactoryResolver
that were attached to it during the instantiation of AgGridNg2
. In the createComponent
method it uses them to resolve a factory for the customization component and instantiate the component:
exportclassNg2FrameworkComponentWrapperextends...{...publiccreateComponent<T>(componentType:{new(...args:any[]):T;}):ComponentRef<T>{letfactory=this.componentFactoryResolver.resolveComponentFactory(componentType);returnthis.viewContainerRef.createComponent(factory);}}
Then using the component reference we get the DOM and attach it to the eGui
private property:
this._componentRef=this.createComponent();this._agAwareComponent=this._componentRef.instance;this._frameworkComponentInstance=this._componentRef.instance;this._eGui=this._componentRef.location.nativeElement;
And that’s it. If you’re interested to learn how we implemented the component resolution process, continue reading.
Component resolution process
ag-Grid is a very complex piece of software. To simplify things internally we’ve designed and implemented our own Dependency Injection (IoC) system that’s modeled after Spring’s IoC container and beans. The component resolution process requires a bunch of services that are registered in this DI system. The most important ones are ComponentResolver and ComponentProvider. Also, we need the Ng2FrameworkComponentWrapper
service that is specific for ag-Grid used as Angular wrapper. It’s registered in the DI system using frameworkComponentWrapper
token.
Resolution is performed through the ComponentResolver
service. When the resolver is instantiated, the frameworkComponentWrapper
and componentProvider
services are attached to the resolver through the DI system and are available on the class instance:
@Bean('componentResolver')exportclassComponentResolver{@Autowired("gridOptions")privategridOptions:GridOptions;@Autowired("componentProvider")privatecomponentProvider:ComponentProvider;@Optional("frameworkComponentWrapper")privateframeworkComponentWrapper:FrameworkComponentWrapper;...}
When the grid needs to instantiate a particular type of a component, e.g. a cell renderer, it calls ComponentResolver.createAgGridComponent
method. The method uses a descriptor of a column to obtain the name of a component that needs to be created. For the cell renderer component the property that contains the name of a component is cellRenderer
:
letcolumnDefs=[{headerName:'Price',field:'price',editable:true,cellRenderer:'numberFormatterComponent'},...]
Once the name is obtained, it is used to retrieve the component class and metadata from the componentProvider
:
exportclassComponentResolver{privateresolveByName(propertyName,...){constcomponentName=componentNameOpt!=null?componentNameOpt:propertyName;constregisteredComponent=this.componentProvider.retrieve(componentName);...}}
The retrieve
method returns the following descriptor of a component:
{component:NumberFormatterComponentdynamicParams:nullsource:1type:Component_Type.Framework}
The type of a component denotes that it’s a framework specific component. All framework components are wrapped into the DynamicAgNg2Component
as explained the first section of the article. Once the component is wrapped, it contains the getGui
method common to all customization components and ag-Grid can work with it as if it’s a plain JavaScript component.