Easier embedding of Angular UI in ag-Grid
A generic cell renderer component to embed template references
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
I’m a big fan of ag-Grid. It’s a fast and powerful JS data-grid library that has great integration with front-end frameworks like Angular (and React, Vue.js, etc.). In this article I’m going to look at a technique I use to make it easier to render bits of Angular UI inside ag-Grid cells.
In the process, we’ll also learn a bit about Angular’s templating functionality. Although Angular is sometimes considered a purely component based framework, it actually has a powerful templating system than can work somewhat independently from components, and is an excellent way to pass fragments of Angular UI between different parts of your application. If you learn how it works, you can use it do some interesting things.
You can find the complete source for this article on StackBlitz: https://stackblitz.com/edit/ag-grid-template-renderer-example
Cell renderer components
Normally when you want to embed Angular-aware UI inside an ag-Grid cell, you create a Cell Renderer Component. This is an Angular component that implements an ag-Grid-defined interface: ICellRendererAngularComp
. You then register this component with ag-Grid’s NgModule
and configure it in the ag-Grid options, using the cellRendererFramework
field for that column. It then serves as a bridge between ag-Grid and Angular: The ag-Grid instance will manage its lifecycle, instantiating it, refreshing it and destroying it as the row data changes and cells are created, modified and destroyed. But the component instances are still running within the Angular framework context, so are still able to fire events within the Angular zone, trigger change detection and interact with services.
For example, If I had an Angular component in my project called StatusIndicatorComponent
which I wanted to embed in ag-Grid, I might write a new renderer called StatusIndicatorRendererComponent
that wrapped it and let me use it in ag-Grid. From then on, in any ag-Grid instance where I wanted to show a StatusIndicatorComponent
, I could use this renderer:
const gridOptions = {
...
colDefs: [
...
{
headerName: 'Status',
colId: 'status',
cellRendererFramework: StatusIndicatorRenderer
}
]
};
This approach is very powerful, and works great for embedding components that you want to use repeatedly throughout your project. However, as I’ve used ag-Grid more and more, I found that I was also running into cases where I wanted to display some Angular UI that was tied quite specifically to a particular instance of ag-Grid and its container component.
For example, maybe I just wanted to include a cell with a handful of action buttons that, when clicked, called a handler on the Angular component that contained the grid. Writing a completely new renderer component for each of these cases was a bit of a chore: Adding it to my project, registering it with ag-Grid, and writing logic to marshal data and events between the renderer and the Angular container component using cell renderer params.
Introducing the template renderer
In order to stop my codebase filling up with lots of single-use cell renderer components, I developed a component called TemplateRendererComponent
. Unlike the cell renderers I’d coded previously, which dealt with embedding specific Angular components into my grid cells, this was a generic renderer, designed to render a template reference and (most importantly) to provide the grid data back to the template as context data.
Here is the complete source code for TemplateRendererComponent
:
The component’s template contains an ng-container
element, with the ngTemplateOutlet
directive applied to it. This directive is included in Angular’s common module, and is part of the public API, although you won’t find any other mention of it in the docs. Which is a shame, because it’s pretty nifty.
What ngTemplateOutlet
does is take a TemplateRef
and uses it to instantiate a new view at the place where the directive is used in the DOM. It also lets us pass in a context
, which is an arbitrary JS object that will be made available to the template when it’s instantiated, and which can be used for data-binding in the template’s HTML.
In this case, the context we want to give it is the row data from ag-Grid. The body of the TemplateRendererComponent
class contains the logic to do that. We’re implementing the ICellRendererAngularComp
interface, which defines some lifecycle methods that ag-Grid will call. The first one is agInit()
:
agInit(params: ICellRendererParams): void {
this.template = params['ngTemplate'];
this.refresh(params);
}
This gives us access to the custom renderer parameters that are defined in the ag-Grid column definition. We’re just looking for a single parameter, ngTemplate
, that we’ll use later to supply the TemplateRef
to instantiate.
The second lifecycle method is refresh()
:
refresh(params: any): boolean {
this.templateContext = {
$implicit: params.data,
params: params
};
return true;
}
This method is called by ag-Grid when the underlying data for the cell changes. In order to make it available to Angular, and our template, we copy it into the templateContext
property of the component, This is the same property that is referenced by the ngTemplateOutlet
directive in the template.
Each field we include in the context becomes available for binding in the template HTML. Notice the use of the special field name $implicit
. This is recognised by the Angular templating engine, and lets us supply a “primary” field that can be bound more easily (we’ll see how later on). Since we’re generally most interested in the params.data
when doing custom rendering, I’ve made that the $implicit
property. But I’m still supplying params
as a separated, named field, so I can access it if necessary.
The next step is to register TemplateRendererComponent
with Angular, and with ag-Grid. We do that in our app’s NgModule
, as follows:
Using the template renderer
In order to use TemplateRendererComponent
we need a template. This can come from anywhere, but the simplest place to put it is in your container component’s template, alongside the ag-Grid element you intend to use it in.
Note: If you haven’t used
ng-template
directly before, it’s a way of declaring a fragment of inert content, much like the<template>
element from regular HTML. Because it’s inert, it won’t show up automatically when your component is rendered, but it can still reference things in your component’s controller, and elsewhere in your template. Whenever you use an Angular directive like*ngIf
or*ngFor
, you’re actually declaring anng-template
, the contents of which is used by Angular to decide what to show when your condition passes, or for each item in your collection, etc.
Here, I’ve declared a simple template with the name greetCell
. Notice the let-row
attribute on the template: That’s how we tell Angular how we want to refer to the $implicit
context data that will be provided to the template by TemplateRendererComponent
. For non-implicit data, we have to give the name of the context field we want to reference. E.g. let-params="params"
.
The body of template contains the content we want to actually appear in each ag-Grid cell. The important thing to understand is that, while it will appear in the grid, it still very much belongs to AppComponent, and that means it can easily reference data and methods that are part of it. Here, the content of the template is a button labelled “Greet” that calls a greet()
method on the component and passes in the row data.
Connecting the template to the grid
The final step to configure our ag-Grid instance itself, which is where we’ll wire the template into the target column. We’ll do that in the AppComponent
class:
Some things to note here:
- We’re injecting the
gridCell
template reference into the component controller using the@ViewChild()
query annotation. - We’re creating the ag-Grid options object early, then using its API to set the column definitions later in the component lifecycle, when we have have access to the
greetCell
template reference. - We’ve defined the
greet()
method — referenced in the button’s(click)
attribute — in the component controller, just like any event handler.
Note: It would be nice if we could set the ag-Grid options and the column definitions at the same time, in an
OnInit
callback. But according to the Angular documentation, the results of view-child queries aren’t set until before thengAfterViewInit
lifecycle stage. Damn.However, if you try it today, in Angular 6, you’ll find that these are queries are set much earlier. So, in fact, the approach of using
OnInit
would work. What’s going on? It seems this early availability is a bug, and will be fixed in a later version of Angular (likely breaking a lot of existing code that inadvertently depends on it!).
Adding more cell templates
Now we have TemplateRendererComponent
, it’s easy to add more cell templates. If we want, these can be more complex, and employ more interaction with the container component.
For example, lets say we wanted to add a cell containing “like” button. When pressed, it would add the country
field from the row data to a list of likes. The list would be limited to 5 entries, and it shouldn’t be possible to like a country more than once.
In order to implement the above, we could add the following cell template:
The liked
array and like()
method referenced by this template would belong to the component controller class, just like any state fields that we wanted to reference from our Angular template.
Conclusion
We’ve seen how using a template renderer can let you easily define fragments of Angular UI that can be embedded into ag-Grid cells, while still interacting closely with your container components.
As with any technique, it’s important to consider when best to use it, and when not to. You should still consider creating separate component renderers if you have UI you want to put into several different ag-Grid instances. Or, if your custom cell has complex logic and state that is mostly self-contained. Putting this logic and state into the container component’s class may bloat it, and detract from its single-responsibility.
That said, I’ve found the template renderer approach useful in quite a few cases in my current app, and I hope that you may get some value from it as well.
You can find the complete source for this article on StackBlitz: https://stackblitz.com/edit/ag-grid-template-renderer-example