Our blazing fast Grid component built with pure JavaScript


Post by annaj »

Hello,

I am trying to override a column's renderer and editor to use some of my own existing angular components.

For now I am experimenting with the configuration. I have:


customElements.define('my-custom-component', createCustomElement(GanttCellRendererComponent, { injector }));

GanttCellRendererComponent has a very simple template of "hello"

My column has the following config:

      htmlEncode: false,
      renderer: () => `<my-custom-component></my-custom-component>`,
      editor: () => `<my-custom-component></my-custom-component>`,
      

The renderer works for me and outputs "hello" into the cell.

The editor does not. When I click into the cell I get the default text editor. Checking the documentation

https://bryntum.com/products/grid/docs/api/Grid/column/Column#config-editor

there is an example for React

editor : ref => <TextEditor ref={ref}/>

Is there something I am missing or is this feature not supported in Angular?

Ideally both attributes would work the same.

Thank you for your help,

Anna


Post by saki »

The editor needs to be a javascript class that has some mandatory methods implemented. These methods are those of a form field:

  • getValue()
  • setValue()
  • isValid()
  • focus()

and perhaps others. This has to be implemented as a JavaScript (TypeScript) class, not as an Angular component.

The rendering part used in this editor should implement html getter that would return a custom element defined in/by Angular.

Although this approach is (theoretically) possible, the simpler approach would be to listen to beforeCellEditStart event, use an Angular component to render itself in the cell and return false from the listener to cancel the default processing.

If you would give us details (screenshots?) of what the editor should do we could be more specific in advising a more specific approach.


Post by annaj »

Thank you for your response Saki.

For context:

We have our own library of form inputs that we use across the site. We also use ag-Grid. Ag-Grid gives us the option to provide angular components to the column configuration - you can provide a component as a cell renderer, you can provide a component as a cell editor.

Here is a column with a select. It is rendered one way in view mode, and when you click into it, it opens up a select.

Implementation in ag-Grid

We like ag-Grid but it doesn't have a Gantt feature. For various reasons we want to be able to reuse our existing components with Bryntum Grid/Gantt - even though there may be parallel widgets provided by Bryntum. We would like there to be minimal differences noticeable to the user when they go between grids rendered by ag-Grid and by Bryntum.

So far I have managed to render our select component into Bryntum Grid. Here is a screenshot, as you can see that's not the correct effect. It is showing the edit view in all the cells.

Result in Bryntum

I have considered making the column "readOnly: true," and then manually implementing code so that when the cell is clicked into the "select view" changes to "select edit". The issue I have with that is keyboard navigation. I have tried out your suggestion to cancel the event and it has the same problem.

Here is the cancel (not finished code obviously, just to check how it works).

    this.ganttInstance.on('beforeCellEditStart', ({ source, editorContext }) => {
      if (editorContext.value === 'Done') {
        return false;
      }
    });


Here is the video of the result

You can see four columns; Start, End, Progress should be editable. Type should be read only.

Tabbing from "Start" to "End" works, tabbing from "End" to "Progress" gets cancelled (because the cell value is 'Done') and the focus goes to "Start" - I want the focus on "Progress". Then on the next row I can tab "Start" to "End" to "Progress" (when the Progress edit event does not get cancelled). If I cancel the event I can't tab into the editable cell, if I don't cancel it I end up seeing the default text editor. This is undesirable.

In summary we want:

  • to render our own angular components in the cells

  • have a different display for view and edit

  • preserve keyboard navigation

Considering these requirements, what approach would you recommend?

Kind regards,

Anna


Post by saki »

We cannot use Angular components directly because the Gantt and its ancestors (Grid, Scheduler, etc.) are written in plain ES6 JavaScript which is agnostic of the framework they are running in. Therefore, we implemented support for frameworks as "wrappers" which are the framework components in fact which encapsulate the JavaScript Bryntum components and widgets. The implementation of these wrappers differs from framework to framework. The main problem here has been how to configure a framework component to render at the proper place and at the proper time.

React has portals, Vue has teleports which can render themselves into a provided html elements but, AFAIK, there is no such possibility in Angular. Therefore we decided to use custom elements which are defined and created in Angular.

The summary considerations then:

You cannot render angular components in the cells directly (ag-Grid has its native Angular version) without converting them to custom elements.

Different look in view and edit mode would be possible (and we use the same way) by aligning the editor (form fields in our case) to the cell size covering its content.

The keyboard navigation would require a debugging/implementation because we have a plethora of various components that can receive focus. We would need a running sample to advise more specifically on this matter.

The final recommendation would be to go with standard Bryntum editor fields which can be extended if it's needed. Their look would be adjusted easily with css. I believe that your Angular counterpart are not very different in their functionality from Bryntum ones. Adjusting their look would probably be the easiest way.


Post by annaj »

Okay, I'm starting to figure things out.

Converting our angular components to custom elements is perfectly fine.

Let's ignore the "Angular Framework" side to this, I'm looking at the MDN docs for custom elements https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements

What I'm struggling with now is how to interface between my custom element and Bryntum. I need to set attributes on it to pass along the values - or if not via the attribute then some other method. Based on your earlier response I've added this to the column and it renders out "<my-custom-component>" into the cell editor.

      editor: {
        html: `<my-custom-component></my-custom-component>`,
        setValue: (value: never) => {
          console.log(value);
        },
        getValue: (value: never) => {
          console.log(value);
          return 'newValue';
        },
      },

I'm not sure that I can find the actual API for this editor object. These are the docs I'm looking at https://bryntum.com/products/grid/docs/api/Core/widget/Field but I can't see anything about "setValue" or "getValue".

What I need is:

  • how do I go from "setValue" to setting an attribute on my custom element?

  • when the new value is submitted how do I pass it on to Bryntum? My hardcoded response for "getValue" doesn't seem to be applied.


Post by saki »

Actually setValue and getValue are the only interface needed for editor. It is briefly explained here (for React).

  • setValue is called by Bryntum when the user initiates the editing (double-click, context menu, ...) the passed value is that of the underlying record (Model) field.

  • user interaction with the editor is not monitored by Bryntum, unless the editor fires events (change, input).

  • the new value is read by Bryntum using getValue method so there should not be any other action needed

  • custom elements can (and usually do) fire events that you can listen to by Angular and/or Bryntum so if anything else besides editor's value is needed it can be implemented using (custom) events. Here is an example:

    import { Component, Input, ElementRef } from '@angular/core';
    
    @Component({
        selector : 'app-color-renderer',
        template : `
            <div class="sample {{ value }}" (click)="onColorClick(value, record)"></div>{{ value }}
        `,
        styleUrls : ['color-renderer.component.scss']
    })
    export class ColorRendererComponent {
    
        @Input() value: string;
        @Input() record: string;
    
        // Convert value to title case
        get colorName(): string {
            let colorName = '';
            if (this.value) {
                colorName = this.value[0].toUpperCase() + this.value.substring(1).toLowerCase();
            }
            return colorName;
        }
    
        // elementRef is needed to dispatch custom events
        constructor(private elementRef: ElementRef) { }
    
        // Internal click listener. Dispatches bubbling custom event.
        onColorClick(value: string, record: string): void {
            const event = new CustomEvent('colorclick', {
                detail     : { value, record, colorName : this.colorName },
                composed   : true,
                bubbles    : true,
                cancelable : true
            });
    
            // Events emitted by Angular EventEmitter cannot bubble, dispatched events can
            this.elementRef.nativeElement.dispatchEvent(event);
        }
    }

You can see this in action in this demo


Post by annaj »

Hello Saki,

I have downloaded the demo you have linked me and expanded upon it here.

I have annotated my changes with "// AJ EDIT"

Here are my issues.

  • setValue gets called on columns[].editor.setValue on line 144 in "grid.config.ts" but not in my EditorRendererComponent. I need to somehow pass the value to EditorRendererComponent.

  • The editor does fire events and I can listen to them - that's fine.

  • getValue on line 147 in "grid.config.ts" never gets called. If I add it to EditorRendererComponent it never gets called. There's a destroy lifecycle hook I can use on the component but I need to know what column/row this edit is coming in for and I haven't been able to pass it to the component.

  • There is weird bug where the first time I edit the cell it will display my renderer, the next time the cell is empty.

Looking forward to your response,

Anna


Post by saki »

Hello Anna,

well, it is not that easy, unfortunately. The editor is expected to be a Bryntum Field or extension thereof but here we are trying to set it as custom element. That won't work because get/setValue are never called as you already found.

The idea is to create a simple "CustomField" that would use the Angular-created custom element as its input field. I have created a ticket https://github.com/bryntum/support/issues/10471 to make such demo. Of course, you can try yourself if you wish.


Post by annaj »

Hello Saki,

A demo will be most useful as I haven't been able to figure it out from the provided documentation. It goes back to what I said in the first post, ideally "renderer" and "editor" will have the same (or similar) interface. I saw in the renderer demo that "tooltipRenderer" and "headerRenderer" also accept callbacks. The current docs for "editor" made it look like it might accept a callback too with the code that was given as an example.


Post by saki »

Hello Anna,

I expect the interface to be similar, but won't be exactly same. I envision something like:

editor: {
    type: 'custominput',
    input: 'angular-implemented-tag',
    // other editor options.
}

The custominput (or however it will be called eventually) would satisfy the existing Bryntum code which expects it to be a Field and calls get/setValue and other methods when needed.

angular-implementd-tag would actually implement these methods, would provide a custom look, feel and behavior of the desired editor. It would be a tag implemented in Angular and converted to custom element.


Post Reply