Our blazing fast Grid component built with pure JavaScript


Post by jintech »

Hi

I have implemented a custom menu column along with the menu event listeners. I had posted some queries regarding this in the forum viewtopic.php?t=27647 which were resolved.
However, there is one issue. On the page load, if we open the actions menu for different rows in the grid a few times, the menu stops showing for those rows. For the first 5 to 6 times we open the actions menu, it shows fine. After that, sometimes it flickers i.e. shows for less than a second and then disappears or does not show up at all. This is happening on the latest bryntum grid demo as well.
I have provided a basic example which can be tested here https://bryntum.com/products/grid/examples/celledit/

Code

import { Menu,Widget, EventHelper, Grid, DataGenerator, DateHelper } from '../../build/grid.module.js?473556';
import shared from '../_shared/shared.module.js?473556';

const menu = new Menu({
		anchor   : false,
		autoShow : false,
		align    : 't-b',
		items    : [
			{
				icon 	 : 'fa-thin fa-info',
				cls 	 : 'viewAssetDetails',
                                html : `<a>View Details<\a>`,
				onItem({source, item}) {
                                        debugger;
					var gridObj = source.up('grid'),
						element = source.parent.owner.element.closest('.b-grid-row'),
						asset = gridObj.getRecordFromElement(element);
				}
			}]
});
// YesNo is a custom button that toggles between Yes and No on click
class YesNo extends Widget {

static get $name() {
    return 'YesNo';
}

// Factoryable type name
static get type() {
    return 'yesno';
}

// Hook up a click listener during construction
construct(config) {
    // Need to pass config to super (Widget) to have things set up properly
    super.construct(config);

    // Handle click on the element
    EventHelper.on({
        element : this.element,
        click   : 'onClick',
        thisObj : this
    });
}

// Always valid, this getter is required by CellEdit feature
get isValid() {
    return true;
}

// Get current value
get value() {
    return Boolean(this._value);
}

// Set current value, updating style
set value(value) {
    this._value = value;

    this.syncInputFieldValue();
}

// Required by CellEdit feature to update display value on language locale change
// Translation is added to examples/_shared/locales/*
syncInputFieldValue() {
    const
        {
            element,
            value
        } = this;

    if (element) {
        element.classList[value ? 'add' : 'remove']('yes');
        element.innerText = value ? this.L('L{Object.Yes}') : this.L('L{Object.No}');
    }
}

// Html for this widget
template() {
    return `<button class="yesno"></button>`;
}

// Click handler
onClick() {
    this.value = !this.value;
}
}

// Register this widget type with its Factory
YesNo.initClass();

let newPlayerCount = 0;

const grid = new Grid({

appendTo : 'container',

features : {
    cellEdit : true,
    sort     : 'name',
    stripe   : true
},

// Show changed cells
showDirty : true,

async validateStartDateEdit({ grid, value }) {
    if (value > DateHelper.clearTime(new Date())) {
        return grid.features.cellEdit.confirm({
            title   : 'Selected date in future',
            message : 'Update field?'
        });
    }
    return true;
},

columns : [
    { text : 'Name', field : 'name', flex : 1 },
    {
        text   : 'Birthplace',
        field  : 'city',
        width  : '8em',
        editor : { type : 'dropdown', items : DataGenerator.cities }
    },
    { text : 'Team', field : 'team', flex : 1 },
    { text : 'Score', field : 'score', editor : 'number', width : '5em' },
    {
        text             : 'Start',
        id               : 'start',
        type             : 'date',
        field            : 'start',
        width            : '9em',
        finalizeCellEdit : 'up.validateStartDateEdit'
    },
    { text : 'Finish (readonly)', type : 'date', field : 'finish', width : '9em', editor : false },
    { text : 'Time', id : 'time', type : 'time', field : 'time', width : '10em' },
    // Column using the custom widget defined above as its editor
    {
        text     : 'Custom', // `text` gets localized automatically, is added to examples/_shared/locales/*
        field    : 'done',
        editor   : 'yesno',
        width    : '5em',
        renderer : ({ value }) => value ? YesNo.L('L{Object.Yes}') : YesNo.L('L{Object.No}')
    },
    { type : 'percent', text : 'Percent', field : 'percent', flex : 1 },
    {
			type: 'widget',
			text: '',
			minWidth: 30,
			maxWidth: 50,
			htmlEncode: false,
			cls: 'text-center hidden-xs',
			cellCls: 'hidden-xs',
			hidden: false, // Set to false to make the column initially visible
			hideable: false, // Set to false to prevent the user from hiding the column
			widgets : [
				{
					type: 'button',
					flex : 1,
					menu,
				}
			]
		}
],

data : DataGenerator.generateData(50),

listeners : {
    selectionChange({ selection }) {
        removeButton.disabled = !selection.length || grid.readOnly;
    }
},

tbar : [
    {
        type        : 'button',
        ref         : 'readOnlyButton',
        text        : 'Read-only',
        tooltip     : 'Toggles read-only mode on grid',
        toggleable  : true,
        icon        : 'b-fa-square',
        pressedIcon : 'b-fa-check-square',
        onToggle    : ({ pressed }) => {
            addButton.disabled = insertButton.disabled = grid.readOnly = pressed;

            removeButton.disabled = pressed || !grid.selectedRecords.length;
        }
    },
    {
        type  : 'buttongroup',
        items : [
            {
                type     : 'button',
                ref      : 'addButton',
                icon     : 'b-fa-plus-circle',
                text     : 'Add',
                tooltip  : 'Adds a new row (at bottom)',
                onAction : () => {
                    const
                        counter = ++newPlayerCount,
                        added   = grid.store.add({
                            name : `New player ${counter}`,
                            cls  : `new_player_${counter}`
                        });

                    grid.selectedRecord = added[0];
                }
            },
            {
                type     : 'button',
                ref      : 'insertButton',
                icon     : 'b-fa-plus-square',
                text     : 'Insert',
                tooltip  : 'Inserts a new row (at top)',
                onAction : () => {
                    const
                        counter = ++newPlayerCount,
                        added   = grid.store.insert(0, {
                            name : `New player ${counter}`,
                            cls  : `new_player_${counter}`
                        });

                    grid.selectedRecord = added[0];
                }
            }
        ]
    },
    {
        type     : 'button',
        ref      : 'removeButton',
        color    : 'b-red',
        icon     : 'b-fa b-fa-trash',
        text     : 'Remove',
        tooltip  : 'Removes selected record(s)',
        disabled : true,
        onAction : () => {
            const selected = grid.selectedRecords;

            if (selected && selected.length) {
                const
                    store      = grid.store,
                    nextRecord = store.getNext(selected[selected.length - 1]),
                    prevRecord = store.getPrev(selected[0]);

                store.remove(selected);
                grid.selectedRecord = nextRecord || prevRecord;
            }
        }
    }
]
});

const { addButton, removeButton, insertButton } = grid.widgetMap;

// Show the dirty marker
grid.store.getAt(0).score = 200;

I don't see any errors on browser console either. Is there anything I am missing here?


Post by marcio »

Hey,

Thanks for the report and the example, it looks strange indeed. I created a ticket to investigate/fix that https://github.com/bryntum/support/issues/8355

Best regards,
Márcio


Post by jintech »

Until this bug gets fixed, is there any alternate way I can implement the action menu which does not have this bug?


Post by marcio »

Perhaps you can try to trigger the menu manually at the click of the action button?

As we still need to investigate the cause of it, we'll need to know that to be able to provide a clear workaround for that.

Best regards,
Márcio


Post by Animal »

I mean as a fix, instead of using one single Menu instance shared between all the button widgets in all the cells, you could just use a menu config block in there so that each button gets its own menu.


Post by jintech »

I was able to temporarily resolve the above issue by adding a click listener inside the widgets and assigning the current element to forElement and target field

click: ({ source: btn }) => {							
	var optionsMenu = btn.menu || (btn.menu = customMenu);
	console.log(optionsMenu.forElement);
	optionsMenu.forElement = btn.currentElement;
	optionsMenu.target = btn.currentElement;
	optionsMenu.show();
}

There is one other issue. I have also used the beforeShowMenu listener in which I am handling some conditions based on which I will be showing certain menu options for each row. This BeforeShowMenu listener is sometimes called more than once for a single click causing a bit of flickering. For the first few times we open the menu, this listener is called only once, but after a few tries it starts getting called multiple times for a single click.

Is there any way I can prevent that from happening?


Post by tasnim »

Hi,

This issue is fixed. The latest release 5.6.6 contains this fix, you can also test it on the demo from your first post.
Please upgrade to the latest version to get the fix.

Here is the ticket https://github.com/bryntum/support/issues/8062

Best of luck :),
Tasnim


Post Reply