Sunday, March 05, 2017

Using a custom input widget in ag-grid

I am currently working with Angular 2 as my front-end framework, and Django at the back end. I have used ag-grid on a couple of different pages and all of the basic functionality works brilliantly. When straying from the happy path though, things get a little tricky. Here I sketch out the solution I came up with to use a custom cell renderer to provide a drop-down list in a cell, and to pass the selected value back to the underlying data structure.

Background


The application I am building is designed to help with the processing of large comma- or tab-separated datafiles. One of the things I need to do is allow the user to select a function to be applied to some of the data in the file, and what parameters to use. My use case for ag-grid is in the definition of this type of calculation. This screenshot should clarify:


The drop-down lists for parameter and data types are created using an Angular 2 component as a custom cell renderer according to the instructions provided here: https://www.ag-grid.com/best-angular-2-data-grid/#gsc.tab=0

Simple updates


The code for the parameter type cell renderer is shown below.

import {Component} from "@angular/core";
import {AgRendererComponent} from "ag-grid-ng2";

@Component({
    moduleId: module.id,
    template: `
        <md-select placeholder="Parameter type" 
                      [(ngModel)]="selectedValue" 
                      (change)="onChange($event)">
                <md-option 
                    ngfor="let parameterType of params.context['parameterType']" 
                    value="{{ parameterType.value }}">
                    {{ parameterType.name }}
                </md-option>
        </md-select>
`
})

export class ParameterTypeCellRendererComponent implements AgRendererComponent {
    private params: any;
    private selectedValue: string;

    constructor(){}

    agInit(params: any): void {
        this.params = params;
        this.selectedValue = this.params['data']['stringParameterType'];
    }

    onChange(event: any) {
        let gridEvent: CustomEvent = new CustomEvent('wa-parameter-type-changed', {
            'bubbles': true,
            'detail': {
                'parameterSequence': this.params.data.parameterSequence,
                'parameterTypeValue': event.value
            }
        });
        event.source.trigger.nativeElement.dispatchEvent(gridEvent);
    }
}

The key point to notice is the onChange event handler. This fires whenever the selected value of the widget changes. Its purpose is to raise a custom event (wa-parameter-type-changed) which bubbles up through the DOM and is detected in the main document. The event listener is shown below.

document.addEventListener('wa-parameter-type-changed', (e) => {
    this.calculation.parameters[e['detail']['parameterSequence']].stringParameterType = 
        e['detail']['parameterTypeValue'];
});

The values contained in the event's detail field are used to identify the value that needs to be updated. The grid's rowData is supplied by this.calculation.parameters which are ordered by their sequence numbers, so once the value has been updated by the listener, the state of the data is consistent with the state of the widget.

Displaying the selected value


A further detail is that md-select does not play well with non-string values. I puzzled over this one for a while. When the fields are already populated - for example, when editing a function - it is important that the drop-downs all display the values that are currently set. This is possible using Angular's standard data binding, but only if the Typescript variables are strings. Using numbers will not work. For this reason, I have ended up with string equivalents of the underlying numeric values throughout the application. It's a bit fiddly, but maintaining the consistency all the way through from Django serialisers to the Angular interface widgets minimises the potential for error. For interest, here is an example of an md-select which uses a data-bound variable to specify the selected value:


<md-select [(ngModel)]="calculation['stringReturnValueDatatype']" placeholder="Return value datatype">
    <md-option *ngFor="let datatype of staticData.datatype" value="{{ datatype.value }}">
        {{ datatype.name }}
    </md-option>
</md-select>



No comments: