Our blazing fast Grid component built with pure JavaScript


Post by jintech »

I want to add a custom menu column for each row in the grid.

For this purpose, I created a custom menu object witth the related options to be shown in the menu as shown in the below code.

const menu = new Menu({
	    anchor   : false,
	    autoShow : false,
	    items    : [
			{
			    icon : 'fa-thin fa-info',
			    cls: 'viewAssetDetails',
			    itemCls: 'viewAssetDetails',
			    text : 'View Details'
			},
	        {
	            icon : 'fa-thin fa-input-text',
	            text : 'Rename'
	        },
	        {
	            icon : 'fa-thin fa-trash',
	            text : 'Delete'
	        },
	        {
	            icon     : 'fa-thin fa-cloud-arrow-down',
	            text     : 'Download'
	        },
	        
], });

I used this menu in a widget column so that the options show when I click on the icon

{
	type: 'widget',
	text: '',
	icon: 'fal fa-ellipsis-v',
	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
	    }
	]
}

There are a few issues here that I am not able to handle.

  1. Each option which I show in the options menu needs to be shown/hidden based on the row data. e.g I should show the download option only if a particular value in that row has a certain value.
  2. The options menu should ideally open in a downward direction as shown below
    expected_menu.png
    expected_menu.png (8.25 KiB) Viewed 597 times
    Currently its showing like this
    current_menu.png
    current_menu.png (11.87 KiB) Viewed 597 times
  3. Thirdly, I am not able to assign a className or id to a menu item. I already have js functions added which pick up the id/class names for the menu items and attach the event to it for performing a certain action.
  4. Also is there a way I can add cutom tags to each item div so that the click events can pick up the required data from them?

Post by alex.l »

Hi,

  1. WidgetColumn has a renderer with access to widgets it contain. You can manage menu items inside renderer
    https://bryntum.com/products/grid/docs/api/Grid/column/WidgetColumn#config-renderer

  2. Menu appears on the side that has a room for it. In case you scroll down and it won't be the space on top, the menu will appear on bottom. Are you sure you want to force the side?

  3. MenuItem has a cls that you can set https://bryntum.com/products/grid/docs/api/Core/widget/MenuItem#config-cls Why can't you do that?

  4. I guess you are trying to add handlers, but why don't you use built-in events and just listen to https://bryntum.com/products/grid/docs/api/Core/widget/Menu#event-item of the menu instead of implementing #3 and #4? Or add onItem method as shown in demo code here https://bryntum.com/products/grid/docs/api/Core/widget/Menu
    You can use html instead of text for menu items.

All the best,
Alex


Post by Animal »

You just need to give the menu a hint as to how it should align: https://bryntum.com/products/grid/docs/api/Core/widget/Menu#config-align

You would like it to use t-b, so use that.

The way to use that config is It's "my edge to the target's edge", so menu's top edge to its owning button's bottom edge.

If there is not enough space below, it will flip to the top. Aligning handles constraining into the viewport.


Post by Animal »

Screenshot 2024-01-18 at 10.50.53.png
Screenshot 2024-01-18 at 10.50.53.png (129.99 KiB) Viewed 581 times

How come there's a scrollbar if your menu only has four items declared?


Post by jintech »

For showing/hiding menu options, I tried the render approach you mentioned. In the widgets object, there is only one item present which is the menu icon which shows up when we load the page i.e. the anchor icon.

widget_option.png
widget_option.png (24.84 KiB) Viewed 567 times

On further investigation of this object, i found the menu items present inside the widget object in the following path

widgets[0].initialConfig.menu.initialConfig.items

I then tried setting the hidden flag for the first option i.e. View details button to true but it does not seem to do anything

widgets[0].initialConfig.menu.initialConfig.items[0].hidden =true;

Post by Animal »

Why not just look at the widget's menu? That's what you are wanting to change isn't it?


Post by jintech »

The thing is I want to hide/show options in a menu based on the current row's values. For example if a row contains a column by the name of start date and I want to show a menu option only if that row's value for start date has a certain value, I should show the option in that particular row's menu. Otherwise hide it

For reference I have provided a similar implementation I want to do.

const menu = new Menu({
	    anchor   : false,
	    autoShow : false,
	    align    : 't-b',
	    items    : [
			{
			    icon : 'fa-thin fa-info',
			    text : 'View Details'
			},
	        {
	            icon : 'fa-thin fa-input-text',
	            text : 'Rename'
	        },
	        {
	            icon : 'fa-thin fa-trash',
	            text : 'Delete'
	        },
	        {
	            icon     : 'fa-thin fa-cloud-arrow-down',
	            text     : 'Download'
	        },
	        {
	            icon : 'fa-thin fa-star',
	            text : 'Add to Favourites'
	        }
	    ],
	});
// 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: '',
			icon: 'fal fa-ellipsis-v',
			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,
			        align: {
			        	align: 't-b'
			        },
			        menu,
			    },
			    
			],
			renderer({ record, widgets }) {
				if (record.score<=0){
				   widgets[0].initialConfig.menu.initialConfig.items[0].hidden =true;
                                    }
                       }
	}
],

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;

In the above code I am trying to hide the view details button if scroe is <=0. This is just an example and each option in the menu may have a different row value its depeds on. For this reason, I need to know for which record I am rendering the menu and then be able to show/hide options accordingly.

The issue is below code does not hide that option in the menu

widgets[0].initialConfig.menu.initialConfig.items[0].hidden =true;

This code can be tested here https://bryntum.com/products/grid/examples/celledit/


Post by marcio »

Hey,

If you configure your button like this, it's working as you described

{
            type       : 'widget',
            text       : '',
            icon       : 'fal fa-ellipsis-v',
            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,
			        align : {
			        	align : 't-b'
			        },
                    listeners : {
                        beforeShowMenu : ({ source, menu }) => {
                            const record = source.owner.getRecordFromElement(source.element);
                            menu.items[0].hidden = record.score <= 0;
                        }
                    },
			        menu
			    }

https://bryntum.com/products/grid/docs/api/Grid/view/GridBase#function-getRecordFromElement
https://bryntum.com/products/grid/docs/api/Core/widget/Button#event-beforeShowMenu

Best regards,
Márcio


Post by jintech »

Thanks Márcio. I was able to implement my logic to show/hide option inside the befoeShowMenu and its working as expected. However in the onItem event for any option, I am not able to get the row data for which the option was selected. I have the item and source param both of which are MenuItem objects with no info on the row record

{
		icon 	 : 'fa-thin fa-info',
		cls 	 : {
			'viewAssetDetails'     : 1
		},
		hidden   : true, 
		text 	 : `<% l('View_details', [], current_user.ui_language) %>`,
		onItem({source, item}) {
					
            }
}

How can I get the curent row record in the onItem event so that I can perform the required functionalitites accordingly.

Important Note: getRecordFromElement function is not present or working in the current version of bryntum grid I am using so an alternate way to fetch the record would be appreciated. Thanks


Post by marcio »

Hey,

Which version are you using? Would be possible to update to the latest version?

Perhaps you can try to use https://bryntum.com/products/grid/docs/api/Grid/view/GridBase#function-getRowFor?

Best regards,
Márcio


Post Reply