We believe that one of the reasons behind ag-Grid’s striking success is how customizable our datagrid is. You can easily extend the default functionality by creating your own custom components and integrating them into the grid. Component based architecture that we use inside ag-Grid significantly simplifies this process. To learn more about customization options, check out this article.
The majority of custom components fed into ag-Grid render some kind of user interface. Besides, there’s an entire set of UI components used to render visual elements on the grid UI. Inside ag-Grid there’s a handy mechanism that allows developers to implement customization components using the framework of their choice. This is often the case when the datagrid is integrated into an application that uses a popular front-end framework. But because our technical team embraced the philosophy of “no dependencies”, we can’t rely on these frameworks to render our internal components.
So we designed and implemented our own component framework and use it to define internal UI components. This article continuous the series on design decisions that help manage complexity inside ag-Grid. Here we’ll explore how and why we implemented our own framework that we use to define and render internal UI components.
Why we built a framework
Although ag-Grid was built using a component based architecture right from the start, for the long time there hasn’t been a unifying way to work with DOM inside the component. Although common sense practices helped standardize on the structure of components, they were still cluttered with repetitive low level code that manipulated DOM elements. DOM API has been developing gradually over years and as a result supported multiple ways to achieve the same thing. One developer could use
querySelector, but somebody else might use
getElementsById. Both would be perfectly valid, but this inconsistency would result into a messy code often with sub-optimal performance.
To give you a concrete example, let’s take a look at the
TextFilter component inside the grid that consists of
input HTML elements:
If you explore the sources for the component, you’ll find a lot of instances of manual DOM manipulation and event listeners handling. Here’s what the
createGui method implemented by the component looked like back then:
As you can see, the code used to rely on browser’s API to query elements and manually add event listeners. As I already mentioned, this approach has a number of downsides. As each component can use different methods to achieve the same thing, it’s a perfect pre-condition for inconsistency between implementations. Event listeners can also be added in different ways, for instance, before or after elements are attached to the DOM. Besides, adding event listeners manually introduces a possibility for a memory leak if they are not removed when the component is not longer rendered on UI.
This all makes it pretty difficult to maintain and reason about the code. As ag-Grid started to take off and the technical team grew, there was a great need for standardization to enforce consistency and efficiency. That’s how the component framework inside ag-Grid was born.
There are many advantages to having a framework. As mentioned earlier, frameworks eliminate the need to write a lot of repetitive code and standardize on the most efficient ways to work with DOM. The advantage of efficiency will never be underestimated. Because frameworks enforce consistency it helps build a project in much less time than would be achieved by writing code without a framework.
The framework inside ag-Grid is developed for specific requirements and use cases that our datagrid handles everyday. For this reason it’s simpler, smaller and easier to work with in comparison to major frontend frameworks like Angular and React.
Because we know our framework in and out, there’s no magic inside we don’t understand. This means that figuring out a problem by debugging takes very little time and no googling.
Finally, as browsers become more and more powerful in what they can do, e.g. development of web components, we can keep improving our framework using new tools and techniques without waiting for major frameworks to adopt them.
Here’s how the presentation logic from the
createGui method we saw above is defined today using the framework:
Instead of querying elements directly with
querySelector, we now use
RefSelector mechanism similar to Ref concept in React. Event listeners are added using
addDestroyableEventListener method that handles unsubscription automatically when parent component is destroyed and in this way prevents memory leaks. The component used to construct DOM from the template manually using
innerHTML browser API:
That logic pretty much remains unchanged today.
Now I want to talk a little bit about how we developed the framework.
Developing a framework
When thinking through the mechanisms our framework should provide to define components and their views, we studied modern web frameworks and took the bits we liked. Similarly to Angular we wanted our framework be template driven, but after several iterations, we decided not over-complicate template syntax and keep the html simple.
Today a template is used to define the DOM structure and is only concerned with layout and styles. One thing we still have in templates is the way to get a reference to the DOM node by marking the node with
ref attribute. The data binding part and event listeners are handled imperatively using methods that include safeguard mechanisms, e.g. automatic unsubscription from events. This approach enabled compile time checking by TypeScript compiler. Finally, components are handled by IoC container to enable them to inject dependencies from the container using auto-wiring.
Here’s the template for
TextFilter that we became familiar with above:
As you can see, there are no event listeners defined in the template, but there are a bunch of
ref attributes that together with
RefSelector annotation help us get a reference to the DOM node in the consistent way. Once we have the references to the underlying DOM nodes we attach listeners manually:
This isn’t the only approach we tried. Actually, over the course of almost 20 version we’ve tried many ways. For example, in the version 10 we used annotations to bind to events like this:
We were experimenting, and we’re happy with the way we handle subscriptions now using
addDestroyableEventListener. The method is implemented by the
BeanStub class that all components inherit from.
I’ve written before about how we implemented our own Dependency Injection mechanism inside ag-Grid. Because we got used to DI container when working with services, we also wanted to benefit from this mechanism when it comes to components. So today the
Autowired annotation is actively used inside components get a dependency service similarly to how the annotation is used in other services. Besides, our IoC container manages lifetime of components and removes all subscriptions when the container is destroyed.
Here’s an example of the component that uses dependency injection:
Well, that’s mostly all information I wanted to share with you about our internal framework. If you would like to play with the framework a little bit, here’s a basic example.
But before you go, there’s one more topic I’d like to cover.
Grid customizations using framework specific components
In the beginning of the article I mentioned that when our datagrid is integrated into an application that uses major frontend framework, developers usually implement grid specific components using framework tools. Specifically, they define the template that a component renders using mechanisms provided by the framework.
For example, a custom cell editor that renders one
The same editor implemented using Angular looks like this:
app-numeric-editor-cell relies on the data binding and event subscription mechanisms implemented by Angular. I chose Angular as an example, but ag-Grid supports all major frameworks — React, Vue, Polymer and even components defined as native web components.
To give you possibility to implement customization components using your favorite framework, we needed to have bridge between ag-Grid and the framework. This bridge is implemented in the form of component adapters. Here’s the diagram that illustrates the setup:
Grid is talking to the framework wrappers, which in turn are responsible for communicating with the underlying framework components. I’ve described this process in great detail for Angular and React in the following articles:
- A step-by-step guide to integrating a third party widget with Angular
If you’re interested in the exact implementation of the adapter components and the bridge mechanism, definitely give these articles a read.