What's new in Grid v5.0.0+
Grid v5.0.0
New JavaScript bundles for combining products
Each product now has a new ES module based JavaScript bundle that only contains the product specific code, called a thin bundle. They are intended to be used when combining multiple products on page, to avoid having shared code loaded multiple times. Previously if you combined for example Grid and TaskBoard on a single page you would import from these bundles (minified size):
grid.module.min.js(~993 kB)taskboard.module.min.js(~1100 kB)
Both products are built upon Bryntum's core library and hence both of them include the core JavaScript (buttons, toolbars, helpers etc.). With thin bundles you would instead import what you need from each product in the hierarchy separately:
core.module.thin.min.js(~643 kB)grid.module.thin.min.js(~268 kB)taskboard.module.thin.min.js(~88 kB)
This way the shared code (from core) is only included once. With the old approach, about 2 MB of JavaScript was loaded, with the new about 1 MB.
The gain (loss actually) will be greater if you combine products that share even more code, like Gantt and Calendar. Calendar builds upon Scheduler > Grid > Core, while Gantt builds upon Scheduler Pro > Scheduler > Grid > Core. When not using thin bundles:
gantt.module.min.js(~1978 kB)calendar.module.min.js(~1833 kB)
With thin bundles (many since these products build upon others):
core.module.thin.min.js(~643 kB)grid.module.thin.min.js(~268 kB)scheduler.module.thin.min.js(~466 kB)schedulerpro.module.thin.min.js(~119 kB)engine.module.thin.min.js(~266 kB)gantt.module.thin.min.js(~122 kB)calendar.module.thin.min.js(~164 kB)
Total ~ 3811 kB vs 2048 kB (1763 kB less).
You import from the thin bundles in the same way as with any other ES modules bundle. The difference compared to using the full bundles is that you have to import from the correct bundle. For example to use the StringHelper class from Core and GridRowModel from Grid, previously you would have something similar to this:
import { StringHelper, GridRowModel } from 'grid.module.js';
Now you have to import them separately (since they are from different bundles):
import { StringHelper } from 'core.module.thin.js';
import { GridRowModel } from 'grid.module.thin.js';
New CSS bundles for combining products
Each theme now has a new CSS bundle that only contains product specific styling, called a thin bundle. They are intended to be used when combining multiple products on page, to avoid having shared styling loaded multiple times. Previously if you combined for example Grid and TaskBoard on a single page, using the Stockholm theme, you would load:
grid.stockholm.css(~244 kB)taskboard.stockholm.css(~243 kB)
Both products are built upon Bryntum's core library and hence both of them include the core CSS (buttons, toolbars, icons etc.). With thin bundles you would instead load each product in the hierarchy separately:
core.stockholm.thin.css(~203 kB)grid.stockholm.thin.css(~40 kB)taskboard.stockholm.thin.css(~40 kB)
This way the shared CSS (from core) is only included once. With the old approach, about 487 kB of CSS was loaded, with the new about 283 kB (204 kB less).
The gain (loss actually) will be greater if you combine products that share even more styling, like Gantt and Calendar. Calendar builds upon Scheduler > Grid > Core, while Gantt builds upon Scheduler Pro > Scheduler > Grid > Core. When not using thin bundles:
gantt.stockholm.css(~659 kB)calendar.stockholm.css(~655 kB)
With thin bundles (many since these products build upon others):
core.stockholm.thin.css(~203 kB)grid.stockholm.thin.css(~40 kB)scheduler.stockholm.thin.css(~363 kB)schedulerpro.stockholm.thin.css(~10 kB)gantt.stockholm.thin.css(~27 kB)calendar.stockholm.thin.css(~47 kB)
Total ~ 1314 kB vs 690 kB (624 kB less).
In your html file, you would for the Gantt + Calendar scenario have something similar to:
<link rel="stylesheet" href="core.stockholm.thin.css" data-bryntum-theme>
<link rel="stylesheet" href="grid.stockholm.thin.css" data-bryntum-theme>
<link rel="stylesheet" href="scheduler.stockholm.thin.css" data-bryntum-theme>
<link rel="stylesheet" href="schedulerpro.stockholm.thin.css" data-bryntum-theme>
<link rel="stylesheet" href="gantt.stockholm.thin.css" data-bryntum-theme>
<link rel="stylesheet" href="calendar.stockholm.thin.css" data-bryntum-theme>
data-bryntum-theme attribute above, it is required if the app will be switching theme at runtime using DomHelper.setTheme(). Grid navigation and accessibility
In keeping with ARIA guidelines, the individual cells of a grid are now focused when navigating cell-to-cell in a grid.
Previously, the outermost element was focused and navigation was purely programmatic. The focused cell was tracked and a class added to it.
Now, a roving focus is used. Cells are not focusable by default, but are assigned a tabIndex when they are required to be focused.
The focusedCell and the events emanating from cell navigation are now driven by focus motion, however that is instigated, rather than as a result of an API request to navigate.
Tabbing onto a grid for the first time focuses the first column leaf header.
Tabbing into a grid that has previously contained focus returns focus to the previously focued cell.
Keyboard navigation
The grid header row is treated as row 1 of the grid, and leaf column headers are the cells in that row.
When a header cell is focused:
- SPACE invokes the column header menu.
- ENTER sorts that column if it is sortable.
- DOWN moves to the first fully visible cell below the header.
Navigation cell-to-cell within the grid proceeds as per ARIA guidelines.
If a cell contains focusable components, they are focused upon entry to that cell by default.
For example in an ActionColumn, when its cells are navigated to, the first button within that cell is focused. TAB works within the cell. ESC pops focus back up onto the encapsulating cell. Arrow keys navigate out to other cells as usual.
Cell editing
Cell editors are now inserted into a cell's DOM so that navigation remains within the edited cell.
Tree grouping
Grid has a new TreeGroup feature that can transform a tree structure at runtime, letting you reconstruct the tree on the fly. It accepts an array of field names or functions that output a value, which it uses to create the parents in the tree. Comes with a new demo, tree-grouping. Try it out in the live demo here:
const grid = new TreeGrid({ appendTo : targetElement, // Makes grid as high as it needs to be to fit rows autoHeight : true, // Initial dataset, will be transformed by the TreeGroup feature store : { fields : [ 'name', 'status', 'prio' ], data : [ { id : 1, name : 'Project 1', expanded : true, children : [ { id : 11, name : 'Task 11', status : 'WIP', prio : 'High' }, { id : 12, name : 'Task 12', status : 'Done', prio : 'Low' }, { id : 13, name : 'Task 13', status : 'Done', prio : 'High' } ] }, { id : 2, name : 'Project 2', expanded : true, children : [ { id : 21, name : 'Task 21', status : 'WIP', prio : 'High' } ] } ] }, columns : [ { type : 'tree', field : 'name', text : 'Name', flex : 1 } ], features : { treeGroup : { levels : ['status'] } }, tbar : [ { type : 'buttongroup', toggleGroup : true, items : [ { text : 'Status', pressed : true, onToggle({ pressed }) { pressed && grid.group(['status']); } }, { text : 'Prio', onToggle({ pressed }) { pressed && grid.group(['prio']); } }, { text : 'Status + Prio', onToggle({ pressed }) { pressed && grid.group(['status', 'prio']); } }, { text : 'none', onToggle({ pressed }) { pressed && grid.clearGroups(); } } ] } ] }); TextAreaField split into two classes
TextAreaField was split into two classes, TextAreaPickerField for use as column cell editor and TextAreaField for use in a form type context.
Simplified test case creation
When reporting a hard to reproduce issue on Bryntum's support forum we often request a test case showing the issue. Getting a good test case greatly reduces the time it takes from reporting the bug until a fix can be released. Worst case we won't be able to find and fix the bug at all without one.
We understand that for complex apps it is not always trivial to produce a standalone test case. The app might be using a lot of different configs and the issue might only appear with a certain dataset etc. To simplify the process of creating a test case we have added a new function called downloadTestCase() to all Bryntum products. Running it collects the current value for the configs your app is using, inlines the current dataset and compiles configs and data into a JavaScript app that is then downloaded.
The app will most likely require manual tweaking before you can submit it to us, but we are hoping it will make creating a test case easier for you. Run grid.downloadTestCase() on the console in a demo to try it. Any feedback on how this could be improved further is welcome on the forums!
Auto-saving state
Grid allows persisting state changes automatically. See updated state demo. Resize columns, rearrange them, sort or filter the store and after refresh application state will be restored, no special action is required.
Grid v5.1.0
Introduction of Create React App templates
Create React App script templates are now available in the public npm repository.
If you are using javascript only, just type:
npx create-react-app my-app --template @bryntum/cra-template-javascript-grid
or if you prefer using typescript:
npx create-react-app my-app --template @bryntum/cra-template-typescript-grid
Note: Please feel free to change my-app to your preferred application name
Check the React integration guide for more information.
Row expander
A RowExpander feature has been added to Grid, which also makes it available for Scheduler and Scheduler Pro. This feature makes it possible to expand and collapse each row by either a separate expander column or a grid cell event. The expanded row's content is rendered by a rendering function that should be provided in the feature config. Comes with a new demo, row-expanding.
Try it out in the live demo here:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The requested URL was not found on this server.</p> </body></html> New module bundle for Angular
Bryntum Grid is now delivered with new ES Module bundle without WebComponents. This was done to avoid conflicts with Angular which also uses WebComponents for applications.
Angular wrappers use grid.module.js bundle in favor of removed grid.lite.umd.js one.
Check the upgrading guide for the details.
New module bundle with WebComponents
Bryntum Grid is now delivered with new grid.wc.module.js ES Module bundle with WebComponents.
Check the upgrading guide for the details.
New keyboard shortcuts functionality
KeyMap is a mixin that allows for standardized and customizable keyboard shortcuts functionality. KeyMap is mixed in to Widget by default and therefore available to all Widget child classes. There is a new guide describing how to customize currently integrated keyboard shortcuts.
Grid v5.1.4
RowReorder feature enhanced
The RowReorder feature now has a dropOnLeaf config which in a TreeGrid enables creation of parents simply by drag and dropping a row onto a leaf. The feature has also been visually updated - when dragging and targeting into a row the targeted row will get a border and a different background. The arrow which previously indicated to which parent a row would be added, has been removed.
Asynchronous store methods for sorting, grouping and filtering
Remote server data loading for store can be configured with sortParamName and filterParamName to pass sorters and filters for loading pre-sorted or pre-filtered data from a remote server.
In case you have configured remote loading, then the methods listed below will now return a Promise. Note that the methods return null if store doesn't use remote loading.
From StoreSort mixin: addSorter, clearSorters, removeSorter, sort.
From StoreGroup mixin: clearGroupers, group, setGroupers.
From StoreFilter mixin: addFilter, clearFilters, filter, filterBy, removeFilter.
Code sample
await store.sort(...);
await store.group(...);
await store.filter(...);
Grid v5.2.0
Collapsible columns
Parent columns (group columns) can now be made collapsible by configuring them with collapsible: true. Collapsible columns renders a collapse / expand button into their headers, clicking it will collapse or expand the column. They can also be toggled using the header context menu, or by programmatically setting column.collapsed = true / false.
There are two collapseModes available:
- 'showFirst' (default) - the first column in the group is always visible, the rest are hidden.
- 'toggleAll' - all columns in the group are collapsed or expanded depending on the current state. Which can be used to show a different set of columns when the group is collapsed or expanded.
const grid = new Grid({ appendTo : targetElement, autoHeight : true, data : DataGenerator.generateData(3), columns : [ { text : 'Employee (toggleAll)', collapsible : true, collapsed : true, collapseMode : 'toggleAll', children : [ { text : 'First name', field : 'firstName', flex : 1 }, { text : 'Surname', field : 'surName', flex : 1 }, { text : 'Name', field : 'name', flex : 1, toggleAllHidden : true } ] }, { text : 'Other Info (showFirst)', collapsible : true, children : [ { text : 'City', field : 'city', width : 150 }, { text : 'Notes', field : 'notes', flex : 2 } ] } ] }); Column renaming
A ColumnRename feature has been added which allows the user to rename columns by either right-clicking column header or using keyboard shortcuts (F2) when column header is focused.
const grid = new Grid({ appendTo : targetElement, // makes grid as high as it needs to be to fit rows autoHeight : true, features : { columnRename : true }, data : DataGenerator.generateData(5), columns : [ { field : 'firstName', text : 'First name', flex : 1 }, { field : 'surName', text : 'Surname', flex : 1 }, { type : 'date', field : 'start', text : 'Start', flex : 1 }, { type : 'date', field : 'finish', text : 'Finish', flex : 1 } ] }); Asynchronous cell editing
CellEdit feature startEditing and finishEditing methods are now async.
finishEditing and cancelEditing methods are now exposed on Grid.
Sample code:
const
grid = new Grid({
appendTo : document.body,
features : {
cellEdit : true
},
columns : [
{ text : 'Name', field : 'name', editor : 'text' }
],
data : [
{ id : 1, name : 'Peter' }
]
}),
testEditinig = async() => {
if (await grid.startEditing({ record : grid.store.first, field : 'name' })) {
console.log('Started editing of name cell');
if (await grid.finishEditing()) {
console.log('Finished editing of name cell');
}
}
};
testEditinig().then(() => {
console.log('Cell editing is ok!');
});
Improved PDF Export feature
Pdf Export feature now renders exported content directly and does not scroll the view anymore. This significantly improves performance (by an order of magnitude) and robustness of the export process. You can enable old behavior by setting enableDirectRendering config on the export feature to false. Old behavior is deprecated and will be removed in the next major release. Please report issues that make you disable direct rendering.
Multi-filter widget
You can now create UI to define and manage a set of CollectionFilters using the GridFieldFilterPickerGroup widget. The widget is configured with a reference to a Grid, and shows a set of filters that filter the Grid's store as they are modified. The widget supports a variety of filtering operators based on the filter's selected data field type, including new date range operators.
The Grid's filter feature can also be configured with isMulti to show the new multi-filter UI in the column header popup.
class RowModel extends GridRowModel { static fields = [{ name : 'name', type : 'string' }, { name : 'age', type : 'number' }, { name : 'city', type : 'string' }, { name : 'food', type : 'string' }]; } const grid = new Grid({ height : '30em', columns : [{ text : 'Name', field : 'name', flex : 2, editor : { type : 'textfield', required : true } }, { text : 'Age', field : 'age', width : 100, type : 'number' }, { text : 'City', field : 'city', flex : 1 }, { text : 'Favorite food', field : 'food', flex : 1 } ], store : new Store({ modelClass : RowModel, data : DataGenerator.generateData(30) }), features : { filter : true }, style : { boxShadow : '0 0 2px rgba(0, 0, 0, 0.2)' } }); const demo = new Container({ appendTo : targetElement, layout : 'vbox', items : { grid, filtersPanel : { type : 'panel', title : 'Filters', maxWidth : '30em', style : { marginBlockStart : '1em' }, items : { filters : { type : 'gridfieldfilterpickergroup', grid, filters : [{ property : 'name', operator : 'startsWith', value : 'M', caseSensitive : false, disabled : true }, { property : 'food', operator : 'isIncludedIn', value : ['Bolognese', 'Pancake'] }] } } } } }); Grid v5.2.6
Support for React Components in tooltips and widgets
React components are now supported in tooltips and widgets. Tooltips have renderer or template function that can now return valid JSX that represents a React component. Widgets can now supply JSX as their html property. For example:
Cell tooltip:
cellTooltipFeature: {
tooltipRenderer: ({ record }) => (
<React.StrictMode>
<DemoTooltip record={record} />
</React.StrictMode>
)
},
React component in widget:
bbar : {
items:[{
type:'widget',
html:<DemoWidget />
}]
},
See the React guide for details.
Grid v5.3.0
AjaxHelper.fetch now supports passing parameters in request body
Since this release AjaxHelper.fetch method can pass provided queryParams in the request body instead of query string. This happens for application/x-www-form-urlencoded and multipart/form-data content types when addQueryParamsToBody option is set to true:
AjaxHelper.fetch('url', {
headers : {
'Content-Type' : 'application/x-www-form-urlencoded'
},
addQueryParamsToBody : true
});
Please check addQueryParamsToBody in FetchOptions
Please note that this behavior is disabled by default so if you need to enable it globally please use AjaxHelper.DEFAULTFETCHOPTIONS:
// enable addQueryParamsToBody by default
AjaxHelper.DEFAULT_FETCH_OPTIONS = {
addQueryParamsToBody : true
}
Fill functionality
The new FillHandle feature brings spreadsheet like fill functionality to the Grid. When enabled, a Grid selection range will get a fill handle along with a fill border. This handle can then be dragged over new cells in any direction. The cells being dragged over will be filled with values calculated from the original selection.
Basic pattern recognition is supported, for example increasing/decreasing numbers or dates, or string patterns.
Try it out in the live demo here:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The requested URL was not found on this server.</p> </body></html> Also, check out our new Spreadsheet demo which includes this feature.
Editing of multiple rows simultaneously
The CellEdit feature has been updated with support for editing multiple rows simultaneously. Simply select multiple rows, edit the last one and then hit Ctrl+Enter to apply the new value to all selected rows. The new multiEdit config is set to true by default.
Try it out in the live demo here:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The requested URL was not found on this server.</p> </body></html> The CellEdit feature has also been migrated into the KeyMap system, which means that all it's keyboard shortcuts are customizable.
Cell copy-paste
The new CellCopyPaste feature adds the ability to cut, copy and paste individual cell values or ranges of cell values. The feature uses the Clipboard API if it is supported by the user agent. This makes it possible to copy ranges of values from external applications like Excel or Google Sheet or paste any text value without the need to open the field editor.
Try it out in the live demo here:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The requested URL was not found on this server.</p> </body></html> Also, check out our new Spreadsheet demo which includes this feature.
Grid selection
The Grid's selection functionality has been greatly improved as of the 5.3 release. In addition to the already existing row and checkbox selection it is now also possible to select:
- Individual or multiple cells
- Whole columns of cells by clicking the column header
- Rows by clicking the RowNumberColumn.
- Ranges of rows or cells by dragging from one row or cell to another.
- Any combination of rows and/or cells by using checkbox selection or rownumber selection when cell selection mode is active.
This is also new:
- Keyboard focused column headers now has same focus indicator as cells.
- All earlier and new selection settings are now changeable at runtime.
- Shift+clicking in checkbox column now selects range of rows.
- The includeChildren settings now affects non-checkbox selection.
- With the new checkboxIndex setting it's possible to change the location of the checkbox selection column.
- Auto-selection of parents by use of the new
includeParentssetting.
For full details of the new selection functionality, please check out or new selection demo
New locales
New locales for 31 languages have been added. Currently available languages are listed in the localization guide.
Grid v5.3.3
Angular IVY and View Engine wrappers
Bryntum Grid now ships with two npm Angular wrappers packages to support different versions of Angular framework.
@bryntum/grid-angular is designed to work with Angular 12 and newer versions, which use the IVY rendering engine.
@bryntum/grid-angular-view is designed to work with Angular 11 and older versions, which use the View Engine for rendering.
Please check Angular integration guide for the additional information.
Grid v5.4.0
Native clipboard support for RowCopyPaste
The RowCopyPaste feature has been enhanced to usa a page-global internal clipboard and also supports the browser's native Clipboard API if accessible. This means that it is possible to copy and paste row between multiple instances of Grid or Grid-based components. It is also possible to copy a row and paste it inside a Spreadsheet app like Excel.
Another improvement is that both the beforeCopy and beforePaste events are now asynchronously preventable.
You can try this out in the Multiple instances demo.
Please note that this enhancement has required API changes:
- The
copyRowsandpasteRowsfunctions are now asynchronous
Widget support in the RowExpander
The RowExpander feature now supports rendering any Bryntum widget inside the expanded row. This makes it possible, for example, to show a detail Grid when expanding a row. Either configure the RowExpander with a widget:
class LineItem extends GridRowModel { static fields = [ 'name', 'quantity', { name : 'price', type : 'number' } ]; // Computed field that sums up the lines total price get total() { return this.price * this.quantity; } } class Order extends GridRowModel { static fields = [ { name : 'date', type : 'date' }, // This is the field from which the expanded LineItemGrid will get its data // The type "store" means that this field has a number of records in itself { name : 'details', type : 'store', storeClass : Store, modelClass : LineItem } ]; // Computed field that sums up the order total get total() { return this.details?.sum('total') || 0; } } const detailsGridConfig = { type : 'grid', // The Grid will adjust its height to fit all rows autoHeight : true, selectionMode : { // Adds a checkbox column that lets the user select rows checkbox : true, // Adds a checkbox to the checkbox column header that lets the user check/uncheck all rows showCheckAll : true }, columns : [ { // A template column that renders an icon next to the product name type : 'template', text : 'Product', field : 'name', flex : 3, template : ({ record }) => `<i class="${record.icon} fa-fw"></i>${record.name}` }, { // A widget column that lets the user increase and decrease the quantity value type : 'number', text : 'Quantity', field : 'quantity', width : 150 }, { text : 'Price', type : 'number', field : 'price', width : 100, format : { style : 'currency', currency : 'USD' } }, { type : 'action', width : 40, actions : [{ cls : 'fa fa-trash', tooltip : 'Delete item', onClick : async({ record }) => { if (await MessageDialog.confirm({ title : 'Please confirm', message : 'Delete this line item?' }) === MessageDialog.yesButton) { record.remove(); } } }] } ] }; const grid = new Grid({ appendTo : targetElement, height : 400, store : { modelClass : Order }, features : { // Enable the feature rowExpander : { dataField : 'details', widget : detailsGridConfig } }, columns : [ { field : 'id', text : 'Order', flex : 1 }, { field : 'date', text : 'Date', type : 'date', flex : 1 }, { type : 'number', field : 'total', text : 'Total', align : 'right', width : 100, format : { style : 'currency', currency : 'USD' } } ], data : [ { id : '123456', date : '2022-12-10', details : [ { id : 111, name : 'Milk', icon : 'fa fa-cow', quantity : 2, price : 1.99 }, { id : 112, name : 'Bread', icon : 'fa fa-bread-slice', quantity : 1, price : 2.49 }, { id : 113, name : 'Eggs', icon : 'fa fa-egg', quantity : 4, price : 0.99 }, { id : 114, name : 'Apples', icon : 'fa fa-apple-whole', quantity : 3, price : 0.69 } ] }, { id : '4368943', date : '2022-12-22', details : [ { id : 122, name : 'Rice', icon : 'fa fa-bowl-rice', quantity : 2, price : 3.49 }, { id : 124, name : 'Lemons', icon : 'fa fa-lemon', quantity : 1, price : 0.69 }, { id : 121, name : 'Peppers', icon : 'fa fa-pepper-hot', quantity : 4, price : 2.99 }, { id : 123, name : 'Cookies', icon : 'fa fa-cookie-bite', quantity : 3, price : 1.99 } ] }, { id : '789012', date : '2022-12-12', details : [ { id : 211, name : 'Chicken', icon : 'fa fa-drumstick-bite', quantity : 2, price : 4.99 }, { id : 212, name : 'Carrots', icon : 'fa fa-carrot', quantity : 1, price : 1.49 }, { id : 213, name : 'Wine', icon : 'fa fa-wine-bottle', quantity : 4, price : 2.99 }, { id : 214, name : 'Cheese', icon : 'fa fa-cheese', quantity : 3, price : 3.49 }, { id : 215, name : 'Bottled Water', icon : 'fa fa-bottle-water', quantity : 5, price : 0.99 } ] } ] }); grid.features.rowExpander.expand(grid.store.first); new Grid({
features : {
rowExpander : {
widget : {
// Custom grid class defined elsewhere
type : 'detailGrid',
// This field is used to populate the detailGrid with data
dataField : 'orderDetails'
}
}
}
});
Or return a widget configuration in the renderer:
new Grid({
features : {
rowExpander : {
async renderer({record, region, expanderElement}){
const myData = await fetch('myURL');
return {
type : 'grid',
autoHeight : true,
columns : [
...
],
data : myData
};
}
}
}
});
You can try it out in the new Master detail demo or the new Nested grids demo.
New color editing widgets
In this release we have added three new widgets that can be used for displaying and editing color values. They are used internally for the Scheduler's and Gantt's new eventColor editors.
ColorPicker
The ColorPicker is meant for usage in a Menu. It comes with a range of default colors, which can easily be overridden.
new Button({ appendTo : targetElement, rendition : 'filled', text : 'Show menu', menu : { colorMenu : { type : 'colorpicker', colorSelected({ color }) { colorBox.style.color = color; } } } }); const colorBox = DomHelper.createElement({ parent : targetElement, class : 'b-color-box', style : { width : '2em', height : '2em', color : '#fff', borderRadius : '4px', marginInlineStart : '1em' } }); Usage example:
new Button({
text : 'Show color menu',
menu : {
items : [
{
text : 'Color',
menu : {
color : {
type : 'colorpicker',
// This overrides the default colors
colors : ['#00FFFF', '#F0FFFF', '#89CFF0', '#0000FF', '#7393B3']
}
}
}
]
}
});
ColorField
The ColorField shows a (optional) label and a colored element. If the user clicks the element, the ColorPicker shows up and lets the user select from a pre-defined range of colors. It uses the ColorPicker's default colors as its default colors.
new ColorField({ labelPosition : 'above', label : 'Color', appendTo : targetElement }); Usage example:
new ColorField({
label : 'Color',
// This overrides the default colors
colors : ['#00FFFF', '#F0FFFF', '#89CFF0', '#0000FF', '#7393B3']
});
ColorColumn
The ColorColumn can be added to any Grid (or Grid-based widget) and used as a display and an editor of a color value. It renders a colored element similar to the ColorField and upon interaction lets the user select from the ColorMenu.
// grid with ColorColumn const grid = new Grid({ appendTo : targetElement, // makes grid as high as it needs to be to fit rows autoHeight : true, data : DataGenerator.generateData(5), columns : [ { field : 'name', text : 'Name', flex : 1 }, { type : 'color', field : 'color', text : 'ColorColumn', width : 100, colors : DataGenerator.colors } ] }); Usage example:
const grid = new Grid({
columns : [
{
type : 'color',
field : 'color',
text : 'ColorColumn',
width : 100,
colors : ['#00FFFF', '#F0FFFF', '#89CFF0', '#0000FF', '#7393B3']
}
]
});
Split grid feature
Grid has a new feature called Split which allows you to split the grid into multiple parts (two or four). This lets you access different parts of the grid that might not otherwise fit in the same viewport. Try it in the live demo below, and in the new split demo:
const grid = new Grid({ appendTo : targetElement, height : '30em', features : { // Enable the Split feature split : true }, data : DataGenerator.generateData(50), columns : [ { field : 'firstName', text : 'First name', width : 150 }, { field : 'surName', text : 'Surname', width : 150 }, { field : 'city', text : 'City', width : 150 }, { type : 'date', field : 'start', text : 'Start', width : 150 }, { type : 'date', field : 'finish', text : 'Finish', width : 150 }, { type : 'number', field : 'score', text : 'Score', width : 150 }, { type : 'number', field : 'age', text : 'Age', width : 150 }, { type : 'rating', field : 'rank', text : 'Rank', width : 150 } ] }); grid.split({ direction : 'vertical' }); Merge cells in any column
The MergeCells feature now supports merging cells in any column, not just sorted columns:
const grid = new Grid({ appendTo : targetElement, height : 320, features : { // Enable the feature mergeCells : { // Merge cells in all columns sortedOnly : false } }, data : DataGenerator.generateData(15), columns : [ { field : 'name', text : 'Name', flex : 1 }, { field : 'color', text : 'Color', flex : 1, mergeCells : true }, { field : 'food', text : 'Favorite food', flex : 1, mergeCells : true } ] }); Opt in to this new behavior by configuring the feature with sortedOnly: false:
const grid = new Grid({
features : {
mergeCells : {
sortedOnly : false
}
}
});
Grid v5.4.1
Support string as a return value of finalizeCellEdit config
The return value of the finalizeCellEdit function can now be a string, which will indicate an error message of the failed validation. This error message will be cleared upon any subsequent user input.
For example for synchronous validation:
const grid = new Grid({
columns : [
{
type : 'text',
text : 'The column',
field : 'someField',
flex : 1,
finalizeCellEdit : ({ value }) => {
return value.length < 4 ? 'Value length should be at least 4 characters' : true;
}
}
]
});
Here we've defined a validation finalizeCellEdit function, which marks all edits with new value less than 4 characters length as invalid.
Grid v5.6.0
New npm packages for combining products
This release introduces a new set of npm packages and framework components, that allows combining multiple Bryntum products in the same application.
These packages contain the product specific code only, as opposed to the current packages that has all code for the products each product builds upon (for example Scheduler contains all code from Grid & Core).
The new packages are called thin packages, and moving forward it will be the recommended way of using Bryntum products in npm based applications (for all supported frameworks). The packages are initially available for licensed users only, but will be made available for trial users in the near future.
The following packages are available:
| Package | Purpose |
|---|---|
| @bryntum/core-thin | Bryntum Core data and UI components package |
| @bryntum/grid-thin | Bryntum Grid component package |
| @bryntum/scheduler-thin | Bryntum Scheduler component package |
| @bryntum/schedulerpro-thin | Bryntum Scheduler Pro component package |
| @bryntum/gantt-thin | Bryntum Gantt component package |
| @bryntum/calendar-thin | Bryntum Calendar component package |
| @bryntum/taskboard-thin | Bryntum TaskBoard component package |
| @bryntum/engine-thin | Bryntum Scheduling engine component package |
Applications should install packages for the products they use, and the products those are built upon (see links to guides below for more information). For example an application using Scheduler Pro should also install Scheduler, Grid & Core:
npm install @bryntum/core-thin @bryntum/grid-thin @bryntum/scheduler-thin @bryntum/schedulerpro-thin
There are also new corresponding wrappers for the supported frameworks, which should be used instead of the current wrappers. For example for React:
| Package | Purpose |
|---|---|
| @bryntum/core-react-thin | Bryntum Core UI widgets React wrappers package |
| @bryntum/grid-react-thin | Bryntum Grid React wrapper package |
| @bryntum/scheduler-react-thin | Bryntum Scheduler React wrapper package |
| @bryntum/schedulerpro-react-thin | Bryntum Scheduler Pro React wrapper package |
| @bryntum/gantt-react-thin | Bryntum Gantt React wrapper package |
| @bryntum/calendar-react-thin | Bryntum Calendar React wrapper package |
| @bryntum/taskboard-react-thin | Bryntum TaskBoard React wrapper package |
Applications should install wrappers only for the products they use, there is no need to install them for the products those are built upon. For example an application using Scheduler Pro:
npm install @bryntum/schedulerpro-react-thin
More information:
Group by array field
Stores can now group by an array field, which means that one record can be a member of more than one group. Linked records are used when a record must appear more than once in a store.
const grid = new Grid({ appendTo : targetElement, // makes grid as high as it needs to be to fit rows autoHeight : true, features : { // group by food group : 'foods' }, store : { fields : [ 'name', { name : 'foods', type : 'array' }, 'city' ] }, data : [ { id : 1, name : 'Bruce Wayne', foods : ['Pizza', 'Sushi', 'Burgers'], city : 'Gotham' }, { id : 2, name : 'Clark Kent', foods : ['Pizza', 'Burgers'], city : 'Metropolis' }, { id : 3, name : 'Barry Allen', foods : ['Pizza', 'Sushi'], city : 'Central City' }, { id : 4, name : 'Diana Prince', foods : ['Burgers', 'Sushi'], city : 'Themyscira' } ], columns : [ { field : 'name', text : 'Name', flex : 1 }, { field : 'foods', text : 'Favorite foods', flex : 1, renderer : ({ record, value }) => { if (record.isGroupHeader) { return value; } return value.join(', '); } }, { field : 'city', text : 'City', flex : 1 } ] }); Drag resizing rows
We have added a new feature called RowResize, that lets users resize rows by dragging their bottom border. It can be configured to allow resizing individual rows, or to apply the same size to all rows. See the updated rowheight example for a demonstration, or try it out below:
const grid = new Grid({ appendTo : targetElement, // makes grid as high as it needs to be to fit rows autoHeight : true, features : { rowResize : true }, data : DataGenerator.generateData(5), columns : [ { field : 'firstName', text : 'First name', flex : 1 }, { field : 'surName', text : 'Surname', flex : 1 }, { type : 'date', field : 'start', text : 'Start', flex : 1 }, { type : 'date', field : 'finish', text : 'Finish', flex : 1 } ] }); Collapsible list groups
The lightweight List widget now supports collapsing / expanding groups:
new SlideToggle({ appendTo : targetElement, label : 'Multiselect', checked : true, labelWidth : '13em', onChange({ checked }) { list.multiSelect = checked; } }); new SlideToggle({ appendTo : targetElement, label : 'Collapsible groups', checked : true, style : 'margin-bottom:1em', labelWidth : '13em', onChange({ checked }) { list.collapsibleGroups = checked; } }); const list = new List({ width : 300, height : 300, appendTo : targetElement, multiSelect : true, displayField : 'name', valueField : 'id', collapsibleGroups : true, allowGroupSelect : false, selected : [1, 2, 4], itemTpl : item => ` <div> <img src="data/Core/images/food/${encodeURIComponent(item.image)}" /> <div> <div>${item.name}</div> <div>${item.desc}</div> </div> <div> ${item.price.toFixed(2)} </div> </div> `, // Show icon based on group name groupHeaderTpl : (record, groupName) => { let icon; switch (groupName) { case 'Drinks': icon = 'wine-bottle'; break; case 'Food': icon = 'pizza-slice'; break; case 'Snacks': icon = 'cookie-bite'; break; } return `<i class="fa-${icon}"></i>${groupName}`; }, store : { fields : [ 'type' ], groupers : [ { field : 'type', ascending : true } ], data : [ { id : 1, name : 'Pizza', type : 'food', desc : 'Fresh from the oven', price : 12.50, image : 'pizza.png' }, { id : 2, name : 'Bacon', type : 'food', desc : 'Crispy and smoky', price : 5.20, image : 'bacon.png' }, { id : 3, name : 'Egg', type : 'food', desc : 'Free range-ish', price : 2.10, image : 'egg.png' }, { id : 4, name : 'Gin tonic', type : 'drinks', desc : 'Classic mix', price : 8.90, image : 'gintonic.png' }, { id : 5, name : 'Wine', type : 'drinks', desc : 'Red, white or sweet', price : 15.00, image : 'redwine.png' }, { id : 6, name : 'Pepsi', type : 'drinks', desc : 'Chilled can', price : 2.70, image : 'pepsi.png' }, { id : 7, name : 'Potato chips', type : 'snacks', desc : 'Salted crunch', price : 3.40, image : 'chips.png' }, { id : 8, name : 'Pretzels', type : 'snacks', desc : 'Twisted delight', price : 2.90, image : 'pretzels.png' }, { id : 9, name : 'Popcorn', type : 'snacks', desc : 'Movie night', price : 3.10, image : 'popcorn.png' }, { id : 10, name : 'Chocolate bar', type : 'snacks', desc : 'At least 5% cocoa', price : 2.50, image : 'chocolate.png' }, { id : 11, name : 'Trail mix', type : 'snacks', desc : 'Nut and raisin blend', price : 4.80, image : 'nuts.png' } ] }, onItem({ item }) { Toast.show('You clicked ' + item.innerText); } }); Default filtering UI changed
The more advanced filtering UI introduced in 5.2.0 (under the isMulti flag) has been made the default for the Filter feature. The previous UI can still be used by specifying legacyMode : true.
targetElement.innerHTML = '<p>Click the filter icon on column headers to apply filters</p>'; const grid = new Grid({ appendTo : targetElement, // makes grid as high as it needs to be to fit rows autoHeight : true, features : { filter : true }, data : DataGenerator.generateData(5), columns : [ { field : 'name', text : 'Traveller', flex : 1 }, { field : 'city', text : 'Visited', flex : 1 }, { field : 'food', text : 'Ate', flex : 1 }, { field : 'rating', text : 'Score', flex : 1 } ] }); RowExpander now supports configuring one widget per region
The RowExpander feature now supports rendering one Bryntum widget inside each of the configured regions of the expanded row. Just provide a widget configuration object for each region like below.
const grid = new Grid({ appendTo : targetElement, height : 400, features : { // Enable the feature rowExpander : { widget : { locked : { type : 'panel', title : 'Actions', items : { addSkill : { type : 'button', text : 'Add skill', width : '100%' }, openReport : { type : 'button', text : 'Open time report', width : '100%' }, deactivate : { type : 'button', text : 'Deactivate', width : '100%' } } }, normal : { type : 'grid', dataField : 'skills', autoHeight : true, columns : [ { field : 'id', text : 'No.', type : 'number', width : 80 }, { field : 'skill', text : 'Skill', flex : 1 }, { field : 'level', text : 'Level', type : 'number', width : 100 }, { field : 'verified', text : 'Verified', type : 'check', width : 100 } ] } } } }, subGridConfigs : { locked : { width : 190 }, normal : { flex : 1 } }, columns : [ { field : 'name', text : 'Name', width : 150, region : 'locked' }, { field : 'id', text : 'Employee no.', type : 'number', width : 110, region : 'normal', align : 'center' }, { field : 'city', text : 'City', width : 110, region : 'normal' }, { field : 'age', text : 'Age', type : 'number', width : 90, region : 'normal', align : 'center' }, { field : 'start', text : 'Start', type : 'date', width : 110, region : 'normal' }, { field : 'name', text : 'Email', width : 220, region : 'normal', htmlEncode : false, renderer : ({ value }) => { const email = value.toLowerCase().replaceAll(' ', '.') + '@example.com'; return StringHelper.xss`<i class="fa-envelope"></i><a href="mailto:${email}">${email}</a>`; } }, { field : 'active', text : 'Active', type : 'check', width : 90, region : 'normal' } ], data : DataGenerator.generateData({ count : 10, addSkills : true, rowCallback(row) { row.skills = row.skills.map((skill, index) => ({ id : index + 1, skill, level : Math.round(Math.random() * 2) + 1, verified : Math.random() > 0.5 })); } }) }); // How to programmatically expand a specific row: //grid.features.rowExpander.expand(grid.store.first); new Grid({
features : {
rowExpander : {
widget : {
locked : {
type : 'detailGrid',
// If your widgets uses different data sources, out the dataField
// property in the widget configuration object
dataField : 'orderDetails'
},
normal : {
type : 'summaryGrid',
dataField : 'sumDetails'
}
}
}
}
});
RowExpander can span over Grid regions
The RowExpander feature now supports rendering an element or a widget that spans over several Grid regions. That element will always have a full Grid width, regardless of any horizontal scroll.
const grid = new Grid({ appendTo : targetElement, height : 400, features : { // Enable the feature rowExpander : { spanRegions : true, widget : { type : 'grid', dataField : 'skills', autoHeight : true, columns : [ { field : 'id', text : 'No.', type : 'number', width : 80 }, { field : 'skill', text : 'Skill', flex : 1 }, { field : 'level', text : 'Level', type : 'number', width : 100 }, { field : 'verified', text : 'Verified', type : 'check', width : 100 } ] } } }, subGridConfigs : { locked : { width : 190 }, normal : { flex : 1 } }, columns : [ { field : 'name', text : 'Name', width : 150, region : 'locked' }, { field : 'id', text : 'Employee no.', type : 'number', width : 110, region : 'normal', align : 'center' }, { field : 'city', text : 'City', width : 110, region : 'normal' }, { field : 'age', text : 'Age', type : 'number', width : 90, region : 'normal', align : 'center' }, { field : 'start', text : 'Start', type : 'date', width : 110, region : 'normal' }, { field : 'name', text : 'Email', width : 220, region : 'normal', htmlEncode : false, renderer : ({ value }) => { const email = value.toLowerCase().replaceAll(' ', '.') + '@example.com'; return StringHelper.xss`<i class="fa-envelope"></i><a href="mailto:${email}">${email}</a>`; } }, { field : 'active', text : 'Active', type : 'check', width : 90, region : 'normal' } ], data : DataGenerator.generateData({ count : 10, addSkills : true, rowCallback(row) { row.skills = row.skills.map((skill, index) => ({ id : index + 1, skill, level : Math.round(Math.random() * 2) + 1, verified : Math.random() > 0.5 })); } }) }); Simply set the spanRegions config to true to activate this behavior. Everything else works the same.
new Grid({
features : {
rowExpander : {
spanRegions : true,
dataField : 'orderDetails',
widget : {
type : 'detailGrid',
}
}
}
});
Tree node animation
When the animateTreeNodeToggle is set to true, tree node expand and collapse operations are animated.
targetElement.innerHTML = '<p>A basic TreeGrid with minimal configuration:</p>'; const tree = new TreeGrid({ appendTo : targetElement, height : 400, data : [ { id : 1, name : 'ABBA', iconCls : 'fa fa-users', born : null, expanded : true, children : [ { id : 11, name : 'Anni-Frid', born : 1945, iconCls : 'fa fa-user' }, { id : 12, name : 'Bjorn', born : 1945, iconCls : 'fa fa-user' }, { id : 13, name : 'Benny', born : 1946, iconCls : 'fa fa-user' }, { id : 14, name : 'Agnetha', born : 1950, iconCls : 'fa fa-user' } ] }, { id : 2, name : 'Roxette', iconCls : 'fa fa-users', born : null, children : [ { id : 21, name : 'Per', born : 1959, iconCls : 'fa fa-user' }, { id : 22, name : 'Marie', born : 1958, iconCls : 'fa fa-user' } ] } ], columns : [ { type : 'tree', field : 'name', text : 'Name', flex : 1 }, { type : 'number', field : 'born', text : 'Born', flex : 1, format : null } ] }); Functions and events declarations for TypeScript have been improved
Declarations of class config/property functions and events (which are represented as onEventName functions) were improved to contain all available parameters and return type.
See examples below:
Old declarations:
/**
* User typed into the field. Please note that the value attached to this event is the raw input field value and
* not the combos value
*/
onInput: Function|string
/**
* Template function that can be used to customize the displayed value
*/
displayValueRenderer: Function
New declarations:
/**
* User typed into the field. Please note that the value attached to this event is the raw input field value and
* not the combos value
*/
onInput: ((event : { source: Combo, value: string, event: Event }) => void)|string
/**
* Template function that can be used to customize the displayed value
*/
displayValueRenderer: (record: Model, combo: Combo) => string|null
Client side print / PDF export
A new Print feature was added to allow printing Grid content using the browser print dialog. It extends the PDFExport feature and uses same configs which manage HTML markup rendering (exporters, columns, paper size, etc.). The only difference is that instead of sending generated HTML to a backend, the feature creates an iframe element with generated content and opens the browser print dialog from it.