Our blazing fast Grid component built with pure JavaScript


Post by peachaws »

Hello team,

I'm currently using Bryntum grid version 5.4.0 with JavaScript implementation, and I have integrated it as a component in my Vue.js page. I have set the props for grid columns, data, and features.

I am attaching the sample video for the requirements I want to achieve. I want to show/hide the two grids with the click of two individual button clicks. Refer to the sample video for the same.

I wondered if there's a way to implement it using the Bryntm grid. Could you provide guidance or suggestions on how to achieve this functionality?

Thank you!

Attachments
Screen Recording 2024-01-24 at 6.03.17 PM.mov
requirement video
(9 MiB) Downloaded 35 times

Post by marcio »

Hey peachaws,

Thanks for reaching out.

By your description and the video that you shared, I believe you'll find this demo useful https://bryntum.com/products/grid/examples/nested-grid/ which works very similar to what you're looking for (having a Grid inside another Grid).

Please let me know your thoughts about that one. :)

Best regards,
Márcio


Post by peachaws »

marcio wrote: Wed Jan 24, 2024 2:58 pm

Hey peachaws,

Thanks for reaching out.

By your description and the video that you shared, I believe you'll find this demo useful https://bryntum.com/products/grid/examples/nested-grid/ which works very similar to what you're looking for (having a Grid inside another Grid).

Please let me know your thoughts about that one. :)

Hello Marcio,

Thank you for your prompt response. Upon reviewing the demo, I observed that it supports nested grids. However, in our scenario, we plan to have two independent grids that will open separately upon clicking two distinct buttons. These two grids will not be nested but will be displayed consecutively, one below the other. Could you please revisit the video for additional clarity on this specific configuration?

Many thanks!


Post by marcio »

Hey,

So, to confirm, as far as I can see in the video, the grids will be displayed "inside" the main grid, in a row, not below the Grid, is that correct?

A possible approach would be to have a container that will have both grids and then programmatically control the display/not display of each grid, which will be controlled by the button clicks.

You can see some information regarding that here https://bryntum.com/products/grid/docs/api/Core/widget/Container

Best regards,
Márcio


Post by peachaws »

marcio wrote: Wed Jan 24, 2024 3:31 pm

Hey,

So, to confirm, as far as I can see in the video, the grids will be displayed "inside" the main grid, in a row, not below the Grid, is that correct?

A possible approach would be to have a container that will have both grids and then programmatically control the display/not display of each grid, which will be controlled by the button clicks.

You can see some information regarding that here https://bryntum.com/products/grid/docs/api/Core/widget/Container

Hi Marcio,

Thanks for your reply. We referred to the provided link. Could you please share an example for the same?
It would be helpful if we could get one to move further with the requirement.

Many thanks!


Post by tasnim »

Hi,

This would look something like this for example:

            widget : {
                type : 'container',
                layout : 'vbox',
                items : {
                    grid : {
                        cls        : 'timerow-grid', // CSS class added to the outer element
                        type       : 'grid',
                        autoHeight : true, // Grid resizes to fit all rows
                        columns    : [
                            { text : 'Project', field : 'name', flex : 1 },
                            { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                            { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                        ],
                        bbar : [ // Bottom toolbar
                            // Add button which adds a row to the expanded grid and starts editing it
                            {
                                text    : 'Add',
                                icon    : 'b-icon-add',
                                onClick : ({ source }) =>  {
                                    const
                                        grid           = source.up('grid'),
                                        expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                        [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                },
                grid2 : {
                    cls        : 'timerow-grid', // CSS class added to the outer element
                    type       : 'grid',
                    hidden     : true,
                    autoHeight : true, // Grid resizes to fit all rows
                    columns    : [
                        { text : 'Project', field : 'name', flex : 1 },
                        { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                        { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                    ],
                    bbar : [ // Bottom toolbar
                        // Add button which adds a row to the expanded grid and starts editing it
                        {
                            text    : 'Add',
                            icon    : 'b-icon-add',
                            onClick : ({ source }) =>  {
                                const
                                    grid           = source.up('grid'),
                                    expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                    [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                }
            }
        }

Post by peachaws »

tasnim wrote: Tue Jan 30, 2024 9:02 am

Hi,

This would look something like this for example:

            widget : {
                type : 'container',
                layout : 'vbox',
                items : {
                    grid : {
                        cls        : 'timerow-grid', // CSS class added to the outer element
                        type       : 'grid',
                        autoHeight : true, // Grid resizes to fit all rows
                        columns    : [
                            { text : 'Project', field : 'name', flex : 1 },
                            { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                            { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                        ],
                        bbar : [ // Bottom toolbar
                            // Add button which adds a row to the expanded grid and starts editing it
                            {
                                text    : 'Add',
                                icon    : 'b-icon-add',
                                onClick : ({ source }) =>  {
                                    const
                                        grid           = source.up('grid'),
                                        expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                        [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                },
                grid2 : {
                    cls        : 'timerow-grid', // CSS class added to the outer element
                    type       : 'grid',
                    hidden     : true,
                    autoHeight : true, // Grid resizes to fit all rows
                    columns    : [
                        { text : 'Project', field : 'name', flex : 1 },
                        { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                        { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                    ],
                    bbar : [ // Bottom toolbar
                        // Add button which adds a row to the expanded grid and starts editing it
                        {
                            text    : 'Add',
                            icon    : 'b-icon-add',
                            onClick : ({ source }) =>  {
                                const
                                    grid           = source.up('grid'),
                                    expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                    [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                }
            }
        }

Hi Tasnim,

Thanks for your reply. We referred to the provided code and attempted to implement it in the example section under the rowexpander and nested grid, but it didn't work for us. We believe we might have missed something. Could you please provide a working example so that it would be easier for us to proceed further?


Post by tasnim »

Hi,

Sure, Please replace the code of this https://bryntum.com/products/grid/examples/nested-grid/ demo with the code below

import { DataGenerator, RandomGenerator, DateHelper, Grid, GridRowModel, Store, StringHelper } from '../../build/grid.module.js?473556';
import shared from '../_shared/shared.module.js?473556';

const randomGenerator = new RandomGenerator();

export const getEmployees = () => {
    const data = DataGenerator.generateData(25);

for (const row of data) {
    DateHelper.add(row.start, row.id * 2, 'month', false);
    row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};

export const getTimeRows = (employees) => {
    const
        tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
        rows  = [];
    let id = 0;

for (const employee of employees) {
    for (let i = 0; i <  Math.ceil(Math.random() * 10); i++) {
        id += 1;
        rows.push({
            id,
            employeeId : employee.id,
            name       : randomGenerator.fromArray(tasks),
            hours      : Math.ceil(Math.random() * 100),
            attested   : Math.random() > 0.5
        });
    }
}
return rows;
};

// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
    static fields = ['id', 'firstName', 'name', 'start', 'email'];

// Example how to set up a "calculated" field
get unattested() {
    return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}

// Example how to set up a "calculated" field
get totalTime() {
    return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}

class TimeRow extends GridRowModel {
    static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];

// The relations config, will set up the relationship between the two stores and model classes
static relations = {
    employee : {
        foreignKey            : 'employeeId', // The id of the "other" record
        foreignStore          : 'employeeStore', // Must be set on the record's store
        relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
    }
};
}

const employeeStore = new Store({
    modelClass : Employee,
    data       : getEmployees()
});

const timeRowStore = new Store({
    modelClass : TimeRow,
    employeeStore, // Must be set, see relations config above
    data       : getTimeRows(employeeStore.records)
});

const grid = new Grid({
    appendTo : 'container',

features : {
           rowExpander : {
        // The widget config declares what type of Widget will be created on each expand
        renderer({ record }) {
            return {
                type : 'container',
                layout : 'vbox',
                items : {
                    grid : {
                        cls        : 'timerow-grid', // CSS class added to the outer element
                        type       : 'grid',
                        autoHeight : true, // Grid resizes to fit all rows
                        columns    : [
                            { text : 'Project', field : 'name', flex : 1 },
                            { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                            { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                        ],
                        bbar : [ // Bottom toolbar
                            // Add button which adds a row to the expanded grid and starts editing it
                            {
                                text    : 'Add',
                                icon    : 'b-icon-add',
                                onClick : ({ source }) =>  {
                                    const
                                        grid           = source.up('grid'),
                                        expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                        [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                    },
                    grid2 : {
                        cls        : 'timerow-grid', // CSS class added to the outer element
                        type       : 'grid',
                        hidden     : true,
                        autoHeight : true, // Grid resizes to fit all rows
                        columns    : [
                            { text : 'Project', field : 'name', flex : 1 },
                            { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                            { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                        ],
                        bbar : [ // Bottom toolbar
                            // Add button which adds a row to the expanded grid and starts editing it
                            {
                                text    : 'Add',
                                icon    : 'b-icon-add',
                                onClick : ({ source }) =>  {
                                    const
                                        grid           = source.up('grid'),
                                        expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                        [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                    grid.startEditing(newRecord);
                                }
                            }, '->', {
                            // Button that sets all rows as "attested"
                                text    : 'Attest all',
                                icon    : 'b-icon-check',
                                onClick : ({ source }) => {
                                    const { store } = source.up('grid');

                                    store.forEach(r => r.attested = true);
                                }
                            }
                        ]
                    }
                }
            }
        },
    }

},

columns : [
    { text : 'Employee no.', field : 'id', width : 100, align : 'center' },
    { text : 'Name', field : 'name', flex : 1 },
    { text : 'Start date', field : 'start', flex : 1, type : 'date' },
    {
        text       : 'Email',
        field      : 'email',
        flex       : 1,
        htmlEncode : false, // To be able to use HTML in the renderer
        renderer   : ({ value }) =>  StringHelper.xss`<i class="b-fa b-fa-envelope"></i><a href="mailto:${value}">${value}</a>`
    }, {
        text   : 'Attested',
        field  : 'unattested',
        width  : 100,
        editor : false,
        renderer({ value, cellElement }) {
            cellElement.style.color = value ? '#e53f2c' : '#4caf50';
            return value ? value + ' left' : 'All done';
        }
    },
    { text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false }
],

store : employeeStore,

listeners : {
    renderRows : ({ source }) => {
        // This will expand third row at a state where the theme has loaded
        source.features.rowExpander.expand(source.store.getAt(2));
    },
    once : true
}
});
grid.on({
    cellClick : ({ column, source, record }) => {
        if (column.field === 'name') {
            const widget = source.features.rowExpander.getExpandedWidgets(record);
            const grid2 = widget.normal.widgetMap.grid2
            grid2.hidden = !grid2.hidden;
            console.log(grid2.hidden);
        }
    }
})

And here is the result

chrome_poueqiqUR2.gif
chrome_poueqiqUR2.gif (3.71 MiB) Viewed 530 times

Here are the docs
https://bryntum.com/products/gantt/docs/api/Grid/feature/RowExpander#config-renderer
https://bryntum.com/products/gantt/docs/api/Grid/feature/RowExpander#function-getExpandedWidgets
https://www.bryntum.com/products/grid/docs/api/Grid/view/mixin/GridElementEvents#event-cellClick

Best of luck :),
Tasnim


Post by peachaws »

tasnim wrote: Wed Jan 31, 2024 11:11 am

Hi,

Sure, Please replace the code of this https://bryntum.com/products/grid/examples/nested-grid/ demo with the code below

import { DataGenerator, RandomGenerator, DateHelper, Grid, GridRowModel, Store, StringHelper } from '../../build/grid.module.js?473556';
import shared from '../_shared/shared.module.js?473556';

const randomGenerator = new RandomGenerator();

export const getEmployees = () => {
    const data = DataGenerator.generateData(25);

for (const row of data) {
    DateHelper.add(row.start, row.id * 2, 'month', false);
    row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};

export const getTimeRows = (employees) => {
    const
        tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
        rows  = [];
    let id = 0;

for (const employee of employees) {
    for (let i = 0; i <  Math.ceil(Math.random() * 10); i++) {
        id += 1;
        rows.push({
            id,
            employeeId : employee.id,
            name       : randomGenerator.fromArray(tasks),
            hours      : Math.ceil(Math.random() * 100),
            attested   : Math.random() > 0.5
        });
    }
}
return rows;
};

// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
    static fields = ['id', 'firstName', 'name', 'start', 'email'];

// Example how to set up a "calculated" field
get unattested() {
    return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}

// Example how to set up a "calculated" field
get totalTime() {
    return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}

class TimeRow extends GridRowModel {
    static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];

// The relations config, will set up the relationship between the two stores and model classes
static relations = {
    employee : {
        foreignKey            : 'employeeId', // The id of the "other" record
        foreignStore          : 'employeeStore', // Must be set on the record's store
        relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
    }
};
}

const employeeStore = new Store({
    modelClass : Employee,
    data       : getEmployees()
});

const timeRowStore = new Store({
    modelClass : TimeRow,
    employeeStore, // Must be set, see relations config above
    data       : getTimeRows(employeeStore.records)
});

const grid = new Grid({
    appendTo : 'container',

features : {
           rowExpander : {
        // The widget config declares what type of Widget will be created on each expand
        renderer({ record }) {
            return {
                type : 'container',
                layout : 'vbox',
                items : {
                    grid : {
                        cls        : 'timerow-grid', // CSS class added to the outer element
                        type       : 'grid',
                        autoHeight : true, // Grid resizes to fit all rows
                        columns    : [
                            { text : 'Project', field : 'name', flex : 1 },
                            { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                            { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                        ],
                        bbar : [ // Bottom toolbar
                            // Add button which adds a row to the expanded grid and starts editing it
                            {
                                text    : 'Add',
                                icon    : 'b-icon-add',
                                onClick : ({ source }) =>  {
                                    const
                                        grid           = source.up('grid'),
                                        expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                        [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                    },
                    grid2 : {
                        cls        : 'timerow-grid', // CSS class added to the outer element
                        type       : 'grid',
                        hidden     : true,
                        autoHeight : true, // Grid resizes to fit all rows
                        columns    : [
                            { text : 'Project', field : 'name', flex : 1 },
                            { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                            { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                        ],
                        bbar : [ // Bottom toolbar
                            // Add button which adds a row to the expanded grid and starts editing it
                            {
                                text    : 'Add',
                                icon    : 'b-icon-add',
                                onClick : ({ source }) =>  {
                                    const
                                        grid           = source.up('grid'),
                                        expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                        [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                    grid.startEditing(newRecord);
                                }
                            }, '->', {
                            // Button that sets all rows as "attested"
                                text    : 'Attest all',
                                icon    : 'b-icon-check',
                                onClick : ({ source }) => {
                                    const { store } = source.up('grid');

                                    store.forEach(r => r.attested = true);
                                }
                            }
                        ]
                    }
                }
            }
        },
    }

},

columns : [
    { text : 'Employee no.', field : 'id', width : 100, align : 'center' },
    { text : 'Name', field : 'name', flex : 1 },
    { text : 'Start date', field : 'start', flex : 1, type : 'date' },
    {
        text       : 'Email',
        field      : 'email',
        flex       : 1,
        htmlEncode : false, // To be able to use HTML in the renderer
        renderer   : ({ value }) =>  StringHelper.xss`<i class="b-fa b-fa-envelope"></i><a href="mailto:${value}">${value}</a>`
    }, {
        text   : 'Attested',
        field  : 'unattested',
        width  : 100,
        editor : false,
        renderer({ value, cellElement }) {
            cellElement.style.color = value ? '#e53f2c' : '#4caf50';
            return value ? value + ' left' : 'All done';
        }
    },
    { text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false }
],

store : employeeStore,

listeners : {
    renderRows : ({ source }) => {
        // This will expand third row at a state where the theme has loaded
        source.features.rowExpander.expand(source.store.getAt(2));
    },
    once : true
}
});
grid.on({
    cellClick : ({ column, source, record }) => {
        if (column.field === 'name') {
            const widget = source.features.rowExpander.getExpandedWidgets(record);
            const grid2 = widget.normal.widgetMap.grid2
            grid2.hidden = !grid2.hidden;
            console.log(grid2.hidden);
        }
    }
})

And here is the result
chrome_poueqiqUR2.gif

Here are the docs
https://bryntum.com/products/gantt/docs/api/Grid/feature/RowExpander#config-renderer
https://bryntum.com/products/gantt/docs/api/Grid/feature/RowExpander#function-getExpandedWidgets
https://www.bryntum.com/products/grid/docs/api/Grid/view/mixin/GridElementEvents#event-cellClick

Best of luck :),
Tasnim

Hi Tasnim,

Thanks for your reply and the provided code. We appreciate your efforts in providing a detailed example but we would also like to explain to you our requirement that we need the expander to work on the button clicks, there would be two buttons, and the click event of each will work separately and expand the particular grid/container. Please see below code snippet and attached screenshot. Could you please help us with that? We have tried it with the same example link that you provided us.
https://bryntum.com/products/grid/examples/nested-grid/

import { DataGenerator, RandomGenerator, DateHelper, Grid, GridRowModel, Store, StringHelper } from '../../build/grid.module.js?474079';
import shared from '../_shared/shared.module.js?474079';


const randomGenerator = new RandomGenerator();

export const getEmployees = () => {
    const data = DataGenerator.generateData(25);

for (const row of data) {
    DateHelper.add(row.start, row.id * 2, 'month', false);
    row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};

export const getTimeRows = (employees) => {
    const
        tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
        rows  = [];
    let id = 0;

for (const employee of employees) {
    for (let i = 0; i <  Math.ceil(Math.random() * 10); i++) {
        id += 1;
        rows.push({
            id,
            employeeId : employee.id,
            name       : randomGenerator.fromArray(tasks),
            hours      : Math.ceil(Math.random() * 100),
            attested   : Math.random() > 0.5
        });
    }
}
return rows;
};

// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
    static fields = ['id', 'firstName', 'name', 'start', 'email'];

// Example how to set up a "calculated" field
get unattested() {
    return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}

// Example how to set up a "calculated" field
get totalTime() {
    return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}

class TimeRow extends GridRowModel {
    static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];

// The relations config, will set up the relationship between the two stores and model classes
static relations = {
    employee : {
        foreignKey            : 'employeeId', // The id of the "other" record
        foreignStore          : 'employeeStore', // Must be set on the record's store
        relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
    }
};
}

const employeeStore = new Store({
    modelClass : Employee,
    data       : getEmployees()
});

const timeRowStore = new Store({
    modelClass : TimeRow,
    employeeStore, // Must be set, see relations config above
    data       : getTimeRows(employeeStore.records)
});

const grid = new Grid({
    appendTo : 'container',

features : {
           rowExpander : {
column: { hidden: true },

    // The widget config declares what type of Widget will be created on each expand
    renderer({ record }) {
        return {
            type : 'container',
            layout : 'vbox',
            items : {
                grid : {
                    cls        : 'timerow-grid', // CSS class added to the outer element
                    type       : 'grid',
                    autoHeight : true, // Grid resizes to fit all rows
                    columns    : [
                        { text : 'Project', field : 'name', flex : 1 },
                        { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                        { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                    ],
                    bbar : [ // Bottom toolbar
                        // Add button which adds a row to the expanded grid and starts editing it
                        {
                            text    : 'Add',
                            icon    : 'b-icon-add',
                            onClick : ({ source }) =>  {
                                const
                                    grid           = source.up('grid'),
                                    expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                    [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                            grid.startEditing(newRecord);
                        }
                    }, '->', {
                    // Button that sets all rows as "attested"
                        text    : 'Attest all',
                        icon    : 'b-icon-check',
                        onClick : ({ source }) => {
                            const { store } = source.up('grid');

                            store.forEach(r => r.attested = true);
                        }
                    }
                ]
                },
                grid2 : {
                    cls        : 'timerow-grid', // CSS class added to the outer element
                    type       : 'grid',
                    hidden     : true,
                    autoHeight : true, // Grid resizes to fit all rows
                    columns    : [
                        { text : 'Project', field : 'name', flex : 1 },
                        { text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
                        { text : 'Attested', field : 'attested', width : 100, type : 'check' }
                    ],
                    bbar : [ // Bottom toolbar
                        // Add button which adds a row to the expanded grid and starts editing it
                        {
                            text    : 'Add',
                            icon    : 'b-icon-add',
                            onClick : ({ source }) =>  {
                                const
                                    grid           = source.up('grid'),
                                    expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                    [newRecord]    = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                        // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                }
            }
        }
    },
}

},

columns : [
    { text : 'Employee no.', field : 'id', width : 100, align : 'center' },
    { text : 'Name', field : 'name', flex : 1 },
    { text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false },
{
            field: "show_details",
            text: "",
            width: 200,
            default: true,
            action: true,
            editor: false,
            type: "widget",
            align: "center",
            widgets: [
              {
                type: "button",
                cls: "b-raised",
                color: "b-green",
                text: "",
                onClick: ({ source: btn }) => {
                  const { record } = btn.cellInfo;
                  this.toggleDetails(record.data, record.parentIndex, "1");
                },
              },
            ],
            renderer: ({ record, widgets }) => {
              widgets[0].text = Boolean(record._toggled) ? "Hide Details" : "Show Details";              if (
                parseInt(record.Matched) === 3 ||
                parseInt(record.Posted) === 1
              ) {
                widgets[0].disabled = true;
              } else {
                widgets[0].disabled = false;
              }
            },
          },
{
            field: "show_detailsnote",
            text: "",
            width: 200,
            default: true,
            action: true,
            editor: false,
            type: "widget",
            align: "center",
            widgets: [
              {
                type: "button",
                cls: "b-raised",
                color: "b-green",
                text: "",
                onClick: ({ source: btn }) => {
                  const { record } = btn.cellInfo;
                  console.log(record);
                  //this.toggleDetailsnote(record, record.parentIndex);
                  // console.log(this.gridConfig.features.rowExpander);
                  // const rowExpander = this.gridConfig.features.rowExpander;                  // if (rowExpander) {
                  //   rowExpander.expand(record);
                  // }
                  // this.gridConfig.features.rowExpander.expand(record);
                },
              },
            ],
            renderer: ({ record, widgets }) => {
              console.log(record._togglednote);
              widgets[0].text = Boolean(record._togglednote)
                ? "Hide Note"
                : "Show Note";
            },
          },
],

store : employeeStore,

listeners : {
    renderRows : ({ source }) => {
        // This will expand third row at a state where the theme has loaded
        source.features.rowExpander.expand(source.store.getAt(2));
    },
    once : true
}
});
grid.on({
    cellClick : ({ column, source, record }) => {
        if (column.field === 'name') {
            const widget = source.features.rowExpander.getExpandedWidgets(record);
            const grid2 = widget.normal.widgetMap.grid2
            grid2.hidden = !grid2.hidden;
            console.log(grid2.hidden);
        }
    }
});
Attachments
example image
example image
btgrd.jpg (501.58 KiB) Viewed 308 times

Post by joakim.l »

Hi.

I built something half-working out of your example code. Here it is:

const randomGenerator = new RandomGenerator();

export const getEmployees = () => {
    const data = DataGenerator.generateData(25);

for (const row of data) {
    DateHelper.add(row.start, row.id * 2, 'month', false);
    row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};

export const getTimeRows = (employees) => {
    const
        tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
        rows  = [];
    let id = 0;

for (const employee of employees) {
    for (let i = 0; i <  Math.ceil(Math.random() * 10); i++) {
        id += 1;
        rows.push({
            id,
            employeeId : employee.id,
            name       : randomGenerator.fromArray(tasks),
            hours      : Math.ceil(Math.random() * 100),
            attested   : Math.random() > 0.5
        });
    }
}
return rows;
};

// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
    static fields = ['id', 'firstName', 'name', 'start', 'email'];

// Example how to set up a "calculated" field
get unattested() {
    return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}

// Example how to set up a "calculated" field
get totalTime() {
    return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}

class TimeRow extends GridRowModel {
    static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];

// The relations config, will set up the relationship between the two stores and model classes
static relations = {
    employee : {
        foreignKey            : 'employeeId', // The id of the "other" record
        foreignStore          : 'employeeStore', // Must be set on the record's store
        relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
    }
};
}

const employeeStore = new Store({
    modelClass : Employee,
    data       : getEmployees()
});

const timeRowStore = new Store({
    modelClass : TimeRow,
    employeeStore, // Must be set, see relations config above
    data       : getTimeRows(employeeStore.records)
});

const grid = new Grid({
    appendTo : 'container',

features : {
    rowExpander : {
        column           : { hidden : true },
        enableAnimations : true,

        // The widget config declares what type of Widget will be created on each expand
        renderer({ record }) {
            const { _show } = record;
            record._show = null;

            if (_show === 'time') {
                return {
                    cls        : 'timerow-grid', // CSS class added to the outer element
                    type       : 'grid',
                    autoHeight : true, // Grid resizes to fit all rows
                    dataField  : 'timeRows',
                    columns    : [
                        {
                            text  : 'Project',
                            field : 'name',
                            flex  : 1
                        },
                        {
                            text  : 'Hours spent',
                            field : 'hours',
                            width : 100,
                            type  : 'number'
                        },
                        {
                            text  : 'Attested',
                            field : 'attested',
                            width : 100,
                            type  : 'check'
                        }
                    ],
                    bbar : [ // Bottom toolbar
                        // Add button which adds a row to the expanded grid and starts editing it
                        {
                            text    : 'Add',
                            icon    : 'b-icon-add',
                            onClick : ({ source }) => {
                                const
                                    grid = source.up('grid'),
                                    expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
                                    [newRecord] = grid.store.add({
                                        name       : null,
                                        hours      : 0,
                                        employeeId : expandedRecord.id
                                    });

                                grid.startEditing(newRecord);
                            }
                        }, '->', {
                            // Button that sets all rows as "attested"
                            text    : 'Attest all',
                            icon    : 'b-icon-check',
                            onClick : ({ source }) => {
                                const { store } = source.up('grid');

                                store.forEach(r => r.attested = true);
                            }
                        }
                    ]
                };
            }

            return {
                cls        : 'timerow-grid', // CSS class added to the outer element
                type       : 'grid',
                autoHeight : true, // Grid resizes to fit all rows
                columns    : [
                    {
                        text  : 'Notes',
                        field : 'name',
                        flex  : 1
                    }
                ]
            };
        }
    }

},

columns : [
    { text : 'Employee no.', field : 'id', width : 100, align : 'center' },
    { text : 'Name', field : 'name', flex : 1 },
    { text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false },
    {
        field   : 'show_details',
        text    : '',
        width   : 200,
        default : true,
        action  : true,
        editor  : false,
        type    : 'widget',
        align   : 'center',
        widgets : [
            {
                type    : 'button',
                cls     : 'b-raised',
                color   : 'b-green',
                text    : '',
                onClick : ({ source: btn }) => {
                    const
                        { record }       = btn.cellInfo,
                        {  rowExpander } = btn.closest('grid').features;

                    record._show = 'time';

                    rowExpander.collapse(record).then(() => {
                        rowExpander.expand(record);
                    });
                }
            }
        ],
        renderer : ({ record, widgets }) => {
            widgets[0].text = Boolean(record._toggled) ? 'Hide Details' : 'Show Details';

            if (
                parseInt(record.Matched) === 3 ||
                parseInt(record.Posted) === 1
            ) {
                widgets[0].disabled = true;
            }
            else {
                widgets[0].disabled = false;
            }
        }
    },
    {
        field   : 'show_detailsnote',
        text    : '',
        width   : 200,
        default : true,
        action  : true,
        editor  : false,
        type    : 'widget',
        align   : 'center',
        widgets : [
            {
                type    : 'button',
                cls     : 'b-raised',
                color   : 'b-green',
                text    : '',
                onClick : ({ source: btn }) => {
                    const
                        { record }       = btn.cellInfo,
                        {  rowExpander } = btn.closest('grid').features;

                    rowExpander.collapse(record).then(() => {
                        rowExpander.expand(record);
                    });
                }
            }
        ],
        renderer : ({ record, widgets }) => {
            console.log(record._togglednote);
            widgets[0].text = Boolean(record._togglednote)
                ? 'Hide Note'
                : 'Show Note';
        }
    }
],

store : employeeStore
});

Some highlights:

  • I'm recreating the expanded grid on each button press. This way you can use the built in widget functionality and map the expanded grid's store to the "outer" row's record's dataField.
  • The rowExpander renderer only returns one grid, not both, for same reason as above.
  • I've disabled animations on the rowExpander feature because it looks bad with this code if you "change" an already expanded record. This could be experimented with.

There is another way of doing this as well, and that is the way tasnim suggested. That way requires you to set the Store and data of each expanded grid.

So, if you're using Model/Store relations and can "connect" the expanded grid to a field on the expanded record, I think the example I gave you would work best.

Regards
Joakim


Post Reply