Custom Editors

Although the FlexGrid provides efficient, Excel-style editing by default, you may want to customize the editing behavior. This example defines a CustomGridEditor class that allows any Wijmo control to be used as a grid editor.

import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import * as wjGrid from '@grapecity/wijmo.grid'; import * as wjInput from '@grapecity/wijmo.input'; import * as wjCore from '@grapecity/wijmo'; // // *** CustomGridEditor class (transpiled from TypeScript) *** // export class CustomGridEditor { /** * Initializes a new instance of a CustomGridEditor. */ constructor(flex, binding, edtClass, options) { // save references this._grid = flex; this._col = flex.columns.getColumn(binding); // create editor this._ctl = new edtClass(document.createElement('div'), options); // connect grid events flex.beginningEdit.addHandler(this._beginningEdit, this); flex.sortingColumn.addHandler(() => { this._commitRowEdits(); }); flex.scrollPositionChanged.addHandler(() => { if (this._ctl.containsFocus()) { flex.focus(); } }); flex.selectionChanging.addHandler((s, e) => { if (e.row != s.selection.row) { this._commitRowEdits(); } }); // connect editor events this._ctl.addEventListener(this._ctl.hostElement, 'keydown', (e) => { switch (e.keyCode) { case wjCore.Key.Tab: case wjCore.Key.Enter: e.preventDefault(); // TFS 255685 this._closeEditor(true); this._grid.focus(); // forward event to the grid so it will move the selection var evt = document.createEvent('HTMLEvents'); evt.initEvent('keydown', true, true); 'altKey,metaKey,ctrlKey,shiftKey,keyCode'.split(',').forEach((prop) => { evt[prop] = e[prop]; }); this._grid.hostElement.dispatchEvent(evt); break; case wjCore.Key.Escape: this._closeEditor(false); this._grid.focus(); break; } }); // close the editor when it loses focus this._ctl.lostFocus.addHandler(() => { setTimeout(() => { if (!this._ctl.containsFocus()) { this._closeEditor(true); // apply edits and close editor this._grid.onLostFocus(); // commit item edits if the grid lost focus } }); }); // commit edits when grid loses focus this._grid.lostFocus.addHandler(() => { setTimeout(() => { if (!this._grid.containsFocus() && !CustomGridEditor._isEditing) { this._commitRowEdits(); } }); }); // open drop-down on f4/alt-down this._grid.addEventListener(this._grid.hostElement, 'keydown', (e) => { // open drop-down on f4/alt-down this._openDropDown = false; if (e.keyCode == wjCore.Key.F4 || (e.altKey && (e.keyCode == wjCore.Key.Down || e.keyCode == wjCore.Key.Up))) { var colIndex = this._grid.selection.col; if (colIndex > -1 && this._grid.columns[colIndex] == this._col) { this._openDropDown = true; this._grid.startEditing(true); e.preventDefault(); } } // commit edits on Enter (in case we're at the last row, TFS 268944) if (e.keyCode == wjCore.Key.Enter) { this._commitRowEdits(); } }, true); // close editor when user resizes the window // REVIEW: hides editor when soft keyboard pops up (TFS 326875) window.addEventListener('resize', () => { if (this._ctl.containsFocus()) { this._closeEditor(true); this._grid.focus(); } }); } // gets an instance of the control being hosted by this grid editor get control() { return this._ctl; } // handle the grid's beginningEdit event by canceling the built-in editor, // initializing the custom editor and giving it the focus. _beginningEdit(grid, args) { // check that this is our column if (grid.columns[args.col] != this._col) { return; } // check that this is not the Delete key // (which is used to clear cells and should not be messed with) var evt = args.data; if (evt && evt.keyCode == wjCore.Key.Delete) { return; } // cancel built-in editor args.cancel = true; // save cell being edited this._rng = args.range; CustomGridEditor._isEditing = true; // initialize editor host var rcCell = grid.getCellBoundingRect(args.row, args.col), rcBody = document.body.getBoundingClientRect(), ptOffset = new wjCore.Point(-rcBody.left, -rcBody.top), zIndex = (args.row < grid.frozenRows || args.col < grid.frozenColumns) ? '3' : ''; wjCore.setCss(this._ctl.hostElement, { position: 'absolute', left: rcCell.left - 1 + ptOffset.x, top: rcCell.top - 1 + ptOffset.y, width: rcCell.width + 1, height: grid.rows[args.row].renderHeight + 1, borderRadius: '0px', zIndex: zIndex, }); // initialize editor content if (!wjCore.isUndefined(this._ctl['text'])) { this._ctl['text'] = grid.getCellData(this._rng.row, this._rng.col, true); } else { throw 'Can\'t set editor value/text...'; } // start editing item var ecv = grid.editableCollectionView, item = grid.rows[args.row].dataItem; if (ecv && item && item != ecv.currentEditItem) { setTimeout(function () { grid.onRowEditStarting(args); ecv.editItem(item); grid.onRowEditStarted(args); }, 50); // wait for the grid to commit edits after losing focus } // activate editor document.body.appendChild(this._ctl.hostElement); this._ctl.focus(); setTimeout(() => { // get the key that triggered the editor var key = (evt && evt.charCode > 32) ? String.fromCharCode(evt.charCode) : null; // get input element in the control var input = this._ctl.hostElement.querySelector('input'); // send key to editor if (input) { if (key) { input.value = key; wjCore.setSelectionRange(input, key.length, key.length); var evtInput = document.createEvent('HTMLEvents'); evtInput.initEvent('input', true, false); input.dispatchEvent(evtInput); } else { input.select(); } } // give the control focus if (!input && !this._openDropDown) { this._ctl.focus(); } // open drop-down on F4/alt-down if (this._openDropDown && this._ctl instanceof wjInput.DropDown) { this._ctl.isDroppedDown = true; this._ctl.dropDown.focus(); } }, 50); } // close the custom editor, optionally saving the edits back to the grid _closeEditor(saveEdits) { if (this._rng) { var flexGrid = this._grid, ctl = this._ctl, host = ctl.hostElement; // raise grid's cellEditEnding event var e = new wjGrid.CellEditEndingEventArgs(flexGrid.cells, this._rng); flexGrid.onCellEditEnding(e); // save editor value into grid if (saveEdits) { if (!wjCore.isUndefined(ctl['value'])) { this._grid.setCellData(this._rng.row, this._rng.col, ctl['value']); } else if (!wjCore.isUndefined(ctl['text'])) { this._grid.setCellData(this._rng.row, this._rng.col, ctl['text']); } else { throw 'Can\'t get editor value/text...'; } this._grid.invalidate(); } // close editor and remove it from the DOM if (ctl instanceof wjInput.DropDown) { ctl.isDroppedDown = false; } host.parentElement.removeChild(host); this._rng = null; CustomGridEditor._isEditing = false; // raise grid's cellEditEnded event flexGrid.onCellEditEnded(e); } } // commit row edits, fire row edit end events (TFS 339615) _commitRowEdits() { var flexGrid = this._grid, ecv = flexGrid.editableCollectionView; this._closeEditor(true); if (ecv && ecv.currentEditItem) { var e = new wjGrid.CellEditEndingEventArgs(flexGrid.cells, flexGrid.selection); ecv.commitEdit(); setTimeout(() => { flexGrid.onRowEditEnding(e); flexGrid.onRowEditEnded(e); flexGrid.invalidate(); }); } } } // document.readyState === 'complete' ? init() : window.onload = init; //# sourceMappingURL=CustomGridEditor.js.map // function init() { // // create some random data var countries = 'US,Germany,UK,Japan,Italy,Greece'.split(','); var products = [ { id: 0, name: 'Widget', unitPrice: 23.43 }, { id: 1, name: 'Gadget', unitPrice: 12.33 }, { id: 2, name: 'Doohickey', unitPrice: 53.07 } ]; var data = []; var dt = new Date(); for (var i = 0; i < 100; i++) { data.push({ id: i, date: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), time: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), country: countries[Math.floor(Math.random() * countries.length)], product: products[Math.floor(Math.random() * products.length)].name, amount: Math.random() * 10000 - 5000, discount: Math.random() / 4 }); } // // grid with custom editors var theGrid = new wjGrid.FlexGrid('#theGrid', { keyActionTab: 'CycleOut', autoGenerateColumns: false, itemsSource: data, columns: [ { header: 'ID', binding: 'id', width: 40, isReadOnly: true }, { header: 'Date', binding: 'date', format: 'd' }, { header: 'Time', binding: 'time', format: 't' }, { header: 'Country', binding: 'country' }, { header: 'Product', binding: 'product' }, { header: 'Amount', binding: 'amount', format: 'n2' } ], }); // // add custom editors to the grid new CustomGridEditor(theGrid, 'date', wjInput.InputDate, { format: 'd' }); new CustomGridEditor(theGrid, 'time', wjInput.InputTime, { format: 't', min: new Date(2000, 1, 1, 7, 0), max: new Date(2000, 1, 1, 22, 0), step: 30 }); new CustomGridEditor(theGrid, 'country', wjInput.ComboBox, { itemsSource: countries }); new CustomGridEditor(theGrid, 'amount', wjInput.InputNumber, { format: 'n2', step: 10 }); // // create an editor based on a ComboBox var multiColumnEditor = new CustomGridEditor(theGrid, 'product', wjInput.ComboBox, { headerPath: 'name', displayMemberPath: 'name', itemsSource: products }); // // customize the ComboBox to show multiple columns var combo = multiColumnEditor.control; combo.listBox.formatItem.addHandler(function (s, e) { e.item.innerHTML = '<table><tr>' + '<td style="width:30px;text-align:right;padding-right:6px">' + e.data.id + '</td>' + '<td style="width:100px;padding-right:6px"><b>' + e.data.name + '</b></td>' + '<td style="width:80px;text-align:right;padding-right:6px">' + wjCore.Globalize.format(e.data.unitPrice, 'c') + '</td>' + '</tr></table>'; }); } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Custom Editors</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div class="container-fluid"> <div id="theGrid"> </div> </div> </body> </html> .wj-flexgrid { height: 300px; margin-bottom: 12px; } .wj-flexgrid .wj-cell { padding: 6px 3px; } body { margin-bottom: 24px; } import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import { Component, enableProdMode, NgModule } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserModule } from '@angular/platform-browser'; import { WjGridModule } from '@grapecity/wijmo.angular2.grid'; import * as wjcCore from '@grapecity/wijmo'; import * as wjcGrid from '@grapecity/wijmo.grid'; import * as wjcInput from '@grapecity/wijmo.input'; import { CustomGridEditor } from './app.customEditor'; @Component({ selector: 'app-component', templateUrl: 'src/app.component.html' }) export class AppComponent { data: any[]; countries = ['US', 'Germany', 'UK' ,'Japan', 'Italy', 'Greece']; products = [ { id: 0, name: 'Widget', unitPrice: 23.43 }, { id: 1, name: 'Gadget', unitPrice: 12.33 }, { id: 2, name: 'Doohickey', unitPrice: 53.07 } ]; // DataSvc will be passed by derived classes constructor() { this.data = this._getData(); } initializeGrid(flex: wjcGrid.FlexGrid) { // add custom editors to the grid new CustomGridEditor(flex, 'date', wjcInput.InputDate, { format: 'd' }); new CustomGridEditor(flex, 'time', wjcInput.InputTime, { format: 't', min: new Date(2000, 1, 1, 7, 0), max: new Date(2000, 1, 1, 22, 0), step: 30 }); new CustomGridEditor(flex, 'country', wjcInput.ComboBox, { itemsSource: this.countries }); new CustomGridEditor(flex, 'amount', wjcInput.InputNumber, { format: 'n2', step: 10 }); // create an editor based on a ComboBox let multiColumnEditor = new CustomGridEditor(flex, 'product', wjcInput.ComboBox, { headerPath: 'name', displayMemberPath: 'name', itemsSource: this.products }); // customize the ComboBox to show multiple columns let combo = <wjcInput.ComboBox>multiColumnEditor.control; combo.listBox.formatItem.addHandler((s: wjcInput.ListBox, e: wjcInput.FormatItemEventArgs) => { e.item.innerHTML = '<table><tr>' + '<td style="width:30px;text-align:right;padding-right:6px">' + e.data.id + '</td>' + '<td style="width:100px;padding-right:6px"><b>' + e.data.name + '</b></td>' + '<td style="width:80px;text-align:right;padding-right:6px">' + wjcCore.Globalize.format(e.data.unitPrice, 'c') + '</td>' + '</tr></table>'; }); } private _getData() { // create some random data let data = []; let dt = new Date(); for (let i = 0; i < 100; i++) { data.push({ id: i, date: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), time: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), country: this.countries[Math.floor(Math.random() * this.countries.length)], product: this.products[Math.floor(Math.random() * this.products.length)].name, amount: Math.random() * 10000 - 5000, discount: Math.random() / 4 }); } return data; } } @NgModule({ imports: [WjGridModule, BrowserModule], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { } enableProdMode(); // Bootstrap application with hash style navigation and global services. platformBrowserDynamic().bootstrapModule(AppModule); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Custom Editors</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Polyfills --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.min.js"></script> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.js"></script> <script src="systemjs.config.js"></script> <script> // workaround to load 'rxjs/operators' from the rxjs bundle System.import('rxjs').then(function (m) { System.set(SystemJS.resolveSync('rxjs/operators'), System.newModule(m.operators)); System.import('./src/app.component'); }); </script> </head> <body> <app-component></app-component> </body> </html> <div class="container-fluid"> <!-- the grid --> <wj-flex-grid #flex [keyActionTab]="'CycleOut'" [(itemsSource)]="data" (initialized)="initializeGrid(flex)"> <wj-flex-grid-column [binding]="'id'" [header]="'ID'" [width]="40" [isReadOnly]="true"></wj-flex-grid-column> <wj-flex-grid-column [binding]="'date'" [header]="'Date'" [format]="'d'"></wj-flex-grid-column> <wj-flex-grid-column [binding]="'time'" [header]="'Time'" [format]="'t'"></wj-flex-grid-column> <wj-flex-grid-column [binding]="'country'" [header]="'Country'"></wj-flex-grid-column> <wj-flex-grid-column [binding]="'product'" [header]="'Product'"></wj-flex-grid-column> <wj-flex-grid-column [binding]="'amount'" [header]="'Amount'" [format]="'n2'"></wj-flex-grid-column> </wj-flex-grid> </div> .wj-flexgrid { max-height: 300px; margin-bottom: 12px; } .wj-flexgrid .wj-cell { padding: 6px 3px; } body { margin-bottom: 24px; } <template> <div class="container-fluid"> <!-- the grid --> <wj-flex-grid keyActionTab="CycleOut" :itemsSource="data" :initialized="initializeGrid"> <wj-flex-grid-column binding="id" header="ID" :width=40 :isReadOnly=true></wj-flex-grid-column> <wj-flex-grid-column binding="date" header="Date" format="d"></wj-flex-grid-column> <wj-flex-grid-column binding="time" header="Time" format="t"></wj-flex-grid-column> <wj-flex-grid-column binding="country" header="Country"></wj-flex-grid-column> <wj-flex-grid-column binding="product" header="Product"></wj-flex-grid-column> <wj-flex-grid-column binding="amount" header="Amount" format="n2"></wj-flex-grid-column> </wj-flex-grid> </div> </template> <script> import "@grapecity/wijmo.styles/wijmo.css"; import "bootstrap.css"; import Vue from "vue"; import "@grapecity/wijmo.vue2.grid"; import * as wjcCore from '@grapecity/wijmo'; import * as wjcGrid from '@grapecity/wijmo.grid'; import * as wjcInput from '@grapecity/wijmo.input'; import { CustomGridEditor } from './customEditor'; new Vue({ el: "#app", data: { data: null, countries: ['US', 'Germany', 'UK' ,'Japan', 'Italy', 'Greece'], products: [ { id: 0, name: 'Widget', unitPrice: 23.43 }, { id: 1, name: 'Gadget', unitPrice: 12.33 }, { id: 2, name: 'Doohickey', unitPrice: 53.07 } ] }, methods:{ initializeGrid(flex){ this.data = this._getData(); // add custom editors to the grid new CustomGridEditor(flex, 'date', wjcInput.InputDate, { format: 'd' }); new CustomGridEditor(flex, 'time', wjcInput.InputTime, { format: 't', min: new Date(2000, 1, 1, 7, 0), max: new Date(2000, 1, 1, 22, 0), step: 30 }); new CustomGridEditor(flex, 'country', wjcInput.ComboBox, { itemsSource: this.countries }); new CustomGridEditor(flex, 'amount', wjcInput.InputNumber, { format: 'n2', step: 10 }); // create an editor based on a ComboBox let multiColumnEditor = new CustomGridEditor(flex, 'product', wjcInput.ComboBox, { headerPath: 'name', displayMemberPath: 'name', itemsSource: this.products }); // customize the ComboBox to show multiple columns let combo = multiColumnEditor.control; combo.listBox.formatItem.addHandler((s, e) => { e.item.innerHTML = '<table><tr>' + '<td style="width:30px;text-align:right;padding-right:6px">' + e.data.id + '</td>' + '<td style="width:100px;padding-right:6px"><b>' + e.data.name + '</b></td>' + '<td style="width:80px;text-align:right;padding-right:6px">' + wjcCore.Globalize.format(e.data.unitPrice, 'c') + '</td>' + '</tr></table>'; }); }, _getData() { // create some random data let data = []; let dt = new Date(); for (let i = 0; i < 100; i++) { data.push({ id: i, date: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), time: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), country: this.countries[Math.floor(Math.random() * this.countries.length)], product: this.products[Math.floor(Math.random() * this.products.length)].name, amount: Math.random() * 10000 - 5000, discount: Math.random() / 4 }); } return data; } } }); </script> <style> .wj-flexgrid { height: 300px; margin-bottom: 12px; } .wj-flexgrid .wj-cell { padding: 6px 3px; } body { margin-bottom: 24px; } </style> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Custom Editors</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app.vue'); </script> </head> <body> <div id="app"> </div> </body> </html> import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './app.css'; // import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as wjGrid from '@grapecity/wijmo.react.grid'; import * as wjcCore from '@grapecity/wijmo'; import * as wjcInput from '@grapecity/wijmo.input'; import { CustomGridEditor } from './customEditor'; class App extends React.Component { constructor(props) { super(props); this.countries = ['US', 'Germany', 'UK', 'Japan', 'Italy', 'Greece']; this.products = [ { id: 0, name: 'Widget', unitPrice: 23.43 }, { id: 1, name: 'Gadget', unitPrice: 12.33 }, { id: 2, name: 'Doohickey', unitPrice: 53.07 } ]; this.state = { data: this.getData() }; } render() { return <div className="container-fluid"> <wjGrid.FlexGrid keyActionTab="CycleOut" itemsSource={this.state.data} initialized={this.initializeGrid.bind(this)}> <wjGrid.FlexGridColumn binding="id" header="ID" width={40} isReadOnly={true}></wjGrid.FlexGridColumn> <wjGrid.FlexGridColumn binding="date" header="Date" format="d"></wjGrid.FlexGridColumn> <wjGrid.FlexGridColumn binding="time" header="Time" format="t"></wjGrid.FlexGridColumn> <wjGrid.FlexGridColumn binding="country" header="Country"></wjGrid.FlexGridColumn> <wjGrid.FlexGridColumn binding="product" header="Product"></wjGrid.FlexGridColumn> <wjGrid.FlexGridColumn binding="amount" header="Amount" format="n2"></wjGrid.FlexGridColumn> </wjGrid.FlexGrid> </div>; } initializeGrid(flex) { new CustomGridEditor(flex, 'date', wjcInput.InputDate, { format: 'd' }); new CustomGridEditor(flex, 'time', wjcInput.InputTime, { format: 't', min: new Date(2000, 1, 1, 7, 0), max: new Date(2000, 1, 1, 22, 0), step: 30 }); new CustomGridEditor(flex, 'country', wjcInput.ComboBox, { itemsSource: this.countries }); new CustomGridEditor(flex, 'amount', wjcInput.InputNumber, { format: 'n2', step: 10 }); // create an editor based on a ComboBox let multiColumnEditor = new CustomGridEditor(flex, 'product', wjcInput.ComboBox, { headerPath: 'name', displayMemberPath: 'name', itemsSource: this.products }); // customize the ComboBox to show multiple columns let combo = multiColumnEditor.control; combo.listBox.formatItem.addHandler((s, e) => { e.item.innerHTML = '<table><tr>' + '<td style="width:30px;text-align:right;padding-right:6px">' + e.data.id + '</td>' + '<td style="width:100px;padding-right:6px"><b>' + e.data.name + '</b></td>' + '<td style="width:80px;text-align:right;padding-right:6px">' + wjcCore.Globalize.format(e.data.unitPrice, 'c') + '</td>' + '</tr></table>'; }); } getData() { // create some random data let data = []; let dt = new Date(); for (let i = 0; i < 100; i++) { data.push({ id: i, date: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), time: new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), country: this.countries[Math.floor(Math.random() * this.countries.length)], product: this.products[Math.floor(Math.random() * this.products.length)].name, amount: Math.random() * 10000 - 5000, discount: Math.random() / 4 }); } return data; } } ReactDOM.render(<App />, document.getElementById('app')); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Custom Editors</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div id="app"></div> </body> </html> .wj-flexgrid { max-height: 300px; margin-bottom: 12px; } .wj-flexgrid .wj-cell { padding: 6px 3px; } body { margin-bottom: 24px; }