Our blazing fast Grid component built with pure JavaScript


Post by jintech »

Hi,
I am trying to use scrollRowIntoView, https://bryntum.com/products/grid/docs/api/Grid/view/GridBase#function-scrollRowIntoView
But its not working

What I want to achieve is that when I click on add, the item is added at the end and it should be available for editing and then be scrolled to that.
I have reproduced it on this example, https://bryntum.com/products/grid/examples/nested-grid/
Please check the onClick listener for add button in tbar.

import shared from '../_shared/shared.module.js?493904';
import { DataGenerator, RandomGenerator, DateHelper, Store, StringHelper, GridRowModel, Grid } from '../../build/grid.module.js?493904';
//region "lib/data.js"

const randomGenerator = new RandomGenerator();

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;
};

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;
};

//endregion

// These two model classes and two stores use the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
    static fields = [
        'id',
        'firstName',
        'name',
        { name : 'start', type : 'date' },
        '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
        propagateRecordChanges : true // This makes the outer Grid react to the expanded Grid(s) changes
    }
};
}

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
        widget : {
            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 : 135, type : 'number', align : 'end' },
                { text : 'Attested', field : 'attested', width : 110, type : 'check' }
            ],
            // Bottom toolbar
            tbar : [
                // Add button, which adds a row to the expanded grid and starts editing it
                {
                    text      : 'Add',
                    icon      : 'b-icon-add',
                    rendition : 'text',
                    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.scrollRowIntoView(newRecord); // This part
                        grid.startEditing(newRecord);
                    }
                },
                '->',
                {
                    // Button that sets all rows as "attested"
                    text      : 'Attest all',
                    icon      : 'b-icon-check',
                    rendition : 'text',
                    onClick   : ({ source }) => {
                        const { store } = source.up('grid');

                        store.forEach(r => r.attested = true);
                    }
                }
            ]
        },
        dataField : 'timeRows' // Tells rowExpander widget to populate it's store from this field
    }
},

columns : [
    { text : 'Employee#', field : 'id', width : 130, 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="fa fa-envelope"></i><a href="mailto:${value}">${value}</a>`
    }, {
        text   : 'Attested',
        field  : 'unattested',
        width  : 110,
        editor : false,
        renderer({ value, cellElement }) {
            cellElement.style.color = value ? '#e53f2c' : '#4caf50';
            return value ? value + ' left' : 'All done';
        }
    },
    { text : 'Total time', field : 'totalTime', width : 120, 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
}
});

Looks like its not working, can you please check why.


Post by tasnim »

Hi,

Thanks for reporting this! The issue is that the nested grid uses autoHeight: true, which means it never overflows. It always expands to fit all its rows. Because of this, scrollRowIntoView on the nested grid computes a zero scroll delta (the row is already "in view" within the nested grid's own body) and does nothing. The outer grid's scroller is never involved.

The fix is to scroll the outer grid instead, passing a Rectangle to bypass the subgrid X-scroller routing:

await grid.await('renderRows');
const row = grid.rowManager.getRowFor(newRecord);
if (row) {
    await outerGrid.scrollable.scrollIntoView(
        Rectangle.from(row.elements.normal),
        { x : false, block : 'nearest' }
    );
}

I've put together a Codepen demo showing the working solution: https://codepen.io/dv-auntik/pen/gbLgJJG?editors=0010

Let me know if you have any questions!

Best regards,
Tasnim

How to ask for help? Please read our Support Policy


Post by jintech »

Thanks, it worked


Post Reply