DevExtreme v24.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

Custom Editors

Different editors can be used to edit cell values in grid columns. The default editor depends on the column configuration. The dependency is illustrated in the editorOptions object's description (this object is used to customize the default editor). In this demo, the SelectBox component is the Status column's default editor, and the editorOptions object is used to specify the component's itemTemplate.

Backend API
<dx-data-grid id="gridContainer" [dataSource]="tasks" [showBorders]="true" (onRowInserted)="$event.component.navigateToRow($event.key)" > <dxo-paging [enabled]="true" [pageSize]="15"></dxo-paging> <dxo-header-filter [visible]="true"></dxo-header-filter> <dxo-search-panel [visible]="true"></dxo-search-panel> <dxo-editing mode="cell" [allowUpdating]="true" [allowAdding]="true"> </dxo-editing> <dxi-column dataField="Owner" [width]="150" [allowSorting]="false" editCellTemplate="singleDropDownBoxEditor" > <dxo-lookup [dataSource]="employees" displayExpr="FullName" valueExpr="ID"> </dxo-lookup> <dxi-validation-rule type="required"></dxi-validation-rule> </dxi-column> <dxi-column dataField="AssignedEmployee" caption="Assignees" [width]="200" [allowSorting]="false" editCellTemplate="tagBoxEditor" [cellTemplate]="cellTemplate" [calculateFilterExpression]="calculateFilterExpression" > <dxo-lookup [dataSource]="employees" valueExpr="ID" displayExpr="FullName"> </dxo-lookup> <dxi-validation-rule type="required"></dxi-validation-rule> </dxi-column> <dxi-column dataField="Subject"> <dxi-validation-rule type="required"></dxi-validation-rule> </dxi-column> <dxi-column dataField="Status" width="200" [editorOptions]="editorOptions"> <dxo-lookup [dataSource]="statuses" displayExpr="name" valueExpr="id"> </dxo-lookup> <dxi-validation-rule type="required"></dxi-validation-rule> </dxi-column> <div *dxTemplate="let cellInfo of 'tagBoxEditor'"> <dx-tag-box [dataSource]="employees" [value]="cellInfo.value" valueExpr="ID" displayExpr="FullName" [showSelectionControls]="true" [maxDisplayedTags]="3" [showMultiTagOnly]="false" [inputAttr]="{ 'aria-label': 'Name' }" applyValueMode="useButtons" [searchEnabled]="true" (onValueChanged)="cellInfo.setValue($event.value)" (onSelectionChanged)="cellInfo.component.updateDimensions()" > </dx-tag-box> </div> <div *dxTemplate="let cellInfo of 'singleDropDownBoxEditor'"> <dx-drop-down-box [dropDownOptions]="dropDownOptions" [dataSource]="employees" [(value)]="cellInfo.value" [inputAttr]="{ 'aria-label': 'Owner' }" displayExpr="FullName" valueExpr="ID" contentTemplate="contentTemplate" > <div *dxTemplate="let e of 'contentTemplate'"> <dx-data-grid [dataSource]="employees" [remoteOperations]="true" [height]="250" [selectedRowKeys]="[cellInfo.value]" [focusedRowEnabled]="true" [focusedRowKey]="cellInfo.value" [hoverStateEnabled]="true" (onSelectionChanged)=" onSelectionChanged($event.selectedRowKeys, cellInfo, e.component) " > <dxi-column dataField="FullName"></dxi-column> <dxi-column dataField="Title"></dxi-column> <dxi-column dataField="Department"></dxi-column> <dxo-paging [enabled]="true" [pageSize]="10"></dxo-paging> <dxo-scrolling mode="virtual"></dxo-scrolling> <dxo-selection mode="single"></dxo-selection> </dx-data-grid> </div> </dx-drop-down-box> </div> <div *dxTemplate="let status of 'statusTemplate'"> <div *ngIf="status === null" ; else elseBlock> <span>(All)</span> </div> <div dx-template #elseBlock> <img src="images/icons/status-{{ status.id }}.svg" class="status-icon middle" /> <span class="middle">{{ status.name }}</span> </div> </div> </dx-data-grid>
import { NgModule, Component, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { DxDataGridModule, DxListModule, DxDropDownBoxModule, DxTagBoxModule, } from 'devextreme-angular'; import { createStore, CustomStore } from 'devextreme-aspnet-data-nojquery'; import { Service, Status } from './app.service'; if (!/localhost/.test(document.location.host)) { enableProdMode(); } @Component({ selector: 'demo-app', templateUrl: 'app/app.component.html', styleUrls: ['app/app.component.css'], providers: [Service], }) export class AppComponent { employees: CustomStore; tasks: CustomStore; statuses: Status[]; dropDownOptions = { width: 500 }; editorOptions = { itemTemplate: 'statusTemplate', }; url = 'https://js.devexpress.com/Demos/Mvc/api/CustomEditors'; constructor(service: Service) { this.statuses = service.getStatuses(); this.tasks = createStore({ key: 'ID', loadUrl: `${this.url}/Tasks`, updateUrl: `${this.url}/UpdateTask`, insertUrl: `${this.url}/InsertTask`, onBeforeSend(method, ajaxOptions) { ajaxOptions.xhrFields = { withCredentials: true }; }, }); this.employees = createStore({ key: 'ID', loadUrl: `${this.url}/Employees`, onBeforeSend(method, ajaxOptions) { ajaxOptions.xhrFields = { withCredentials: true }; }, }); } onSelectionChanged(selectedRowKeys, cellInfo, dropDownBoxComponent) { cellInfo.setValue(selectedRowKeys[0]); if (selectedRowKeys.length > 0) { dropDownBoxComponent.close(); } } calculateFilterExpression(filterValue, selectedFilterOperation, target) { if (target === 'search' && typeof (filterValue) === 'string') { return [(this as any).dataField, 'contains', filterValue]; } return function (rowData) { return (rowData.AssignedEmployee || []).indexOf(filterValue) !== -1; }; } cellTemplate(container, options) { const noBreakSpace = '\u00A0'; const assignees = (options.value || []).map( (assigneeId: number) => options.column!.lookup!.calculateCellValue!(assigneeId), ); const text = assignees.join(', '); container.textContent = text || noBreakSpace; container.title = text; } } @NgModule({ imports: [ BrowserModule, DxDataGridModule, DxListModule, DxDropDownBoxModule, DxTagBoxModule, ], declarations: [AppComponent], bootstrap: [AppComponent], }) export class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);
.status-icon { height: 16px; width: 16px; display: inline-block; margin-right: 8px; } .middle { vertical-align: middle; }
import { Injectable } from '@angular/core'; export class Status { id: number; name: string; } const statuses: Status[] = [{ id: 1, name: 'Not Started', }, { id: 2, name: 'In Progress', }, { id: 3, name: 'Deferred', }, { id: 4, name: 'Need Assistance', }, { id: 5, name: 'Completed', }, ]; @Injectable() export class Service { getStatuses() { return statuses; } }
// In real applications, you should not transpile code in the browser. // You can see how to create your own application with Angular and DevExtreme here: // https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Create_a_DevExtreme_Application/ const componentNames = [ 'accordion', 'action-sheet', 'autocomplete', 'bar-gauge', 'box', 'bullet', 'button-group', 'button', 'calendar', 'chart', 'check-box', 'circular-gauge', 'color-box', 'context-menu', 'data-grid', 'date-box', 'date-range-box', 'defer-rendering', 'diagram', 'draggable', 'drawer', 'drop-down-box', 'drop-down-button', 'file-manager', 'file-uploader', 'filter-builder', 'form', 'funnel', 'gallery', 'gantt', 'html-editor', 'linear-gauge', 'list', 'load-indicator', 'load-panel', 'lookup', 'map', 'menu', 'multi-view', 'nested', 'number-box', 'pie-chart', 'pivot-grid-field-chooser', 'pivot-grid', 'polar-chart', 'popover', 'popup', 'progress-bar', 'radio-group', 'range-selector', 'range-slider', 'recurrence-editor', 'resizable', 'responsive-box', 'sankey', 'scheduler', 'scroll-view', 'select-box', 'slider', 'sortable', 'sparkline', 'speed-dial-action', 'splitter', 'switch', 'tab-panel', 'tabs', 'tag-box', 'text-area', 'text-box', 'tile-view', 'toast', 'toolbar', 'tooltip', 'tree-list', 'tree-map', 'tree-view', 'validation-group', 'validation-summary', 'validator', 'vector-map', ]; window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, }, meta: { 'typescript': { 'exports': 'ts', }, 'devextreme-aspnet-data-nojquery': { 'esModule': true, }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, '@angular/platform-browser-dynamic': { 'esModule': true, }, '@angular/platform-browser': { 'esModule': true, }, '@angular/core': { 'esModule': true, }, '@angular/common': { 'esModule': true, }, '@angular/animations': { 'esModule': true, }, '@angular/forms': { 'esModule': true, }, }, paths: { 'npm:': 'https://unpkg.com/', 'bundles:': '../../../../bundles/', }, map: { 'ts': 'npm:plugin-typescript@4.2.4/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', /* @angular */ '@angular/compiler': 'bundles:@angular/compiler.umd.js', '@angular/platform-browser-dynamic': 'bundles:@angular/platform-browser-dynamic.umd.js', '@angular/core': 'bundles:@angular/core.umd.js', '@angular/core/primitives/signals': 'bundles:@angular/core.primitives.signals.umd.js', '@angular/common': 'bundles:@angular/common.umd.js', '@angular/common/http': 'bundles:@angular/common-http.umd.js', '@angular/platform-browser': 'bundles:@angular/platform-browser.umd.js', '@angular/platform-browser/animations': 'bundles:@angular/platform-browser.umd.js', '@angular/forms': 'bundles:@angular/forms.umd.js', /* devextreme */ 'devextreme': 'npm:devextreme@24.1.3/cjs', '@devextreme/runtime': 'npm:@devextreme/runtime@3.0.13', 'devextreme/bundles/dx.all': 'npm:devextreme@24.1.3/bundles/dx.all.js', 'devextreme-quill': 'npm:devextreme-quill@1.7.1/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.8', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.56', /* devextreme-angular umd maps */ 'devextreme-angular': 'bundles:devextreme-angular/devextreme-angular.umd.js', 'devextreme-angular/core': 'bundles:devextreme-angular/devextreme-angular-core.umd.js', ...componentNames.reduce((acc, name) => { acc[`devextreme-angular/ui/${name}`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}.umd.js`; return acc; }, {}), 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'tslib': 'npm:tslib@2.6.1/tslib.js', 'rxjs': 'npm:rxjs@7.5.3/dist/bundles/rxjs.umd.js', 'rxjs/operators': 'npm:rxjs@7.5.3/dist/cjs/operators/index.js', 'devextreme-aspnet-data-nojquery': 'npm:devextreme-aspnet-data-nojquery@3.0.0/index.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@1.28.1/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign@1.1.0', 'inferno': 'npm:inferno@7.4.11/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@7.4.11/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate@7.4.11/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'app': { main: './app.component.ts', defaultExtension: 'ts', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, 'rxjs': { defaultExtension: 'js', }, 'rxjs/operators': { defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:@devextreme/runtime@3.0.13/inferno/package.json', 'npm:rxjs@7.5.3/package.json', 'npm:rxjs@7.5.3/operators/package.json', 'npm:devexpress-diagram@2.2.8/package.json', 'npm:devexpress-gantt@4.1.56/package.json', ], }; System.config(window.config); // System.import('@angular/compiler').catch(console.error.bind(console));
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/24.1.3/css/dx.light.css" /> <script src="https://unpkg.com/core-js@2.6.12/client/shim.min.js"></script> <script src="https://unpkg.com/zone.js@0.13.0/dist/zone.js"></script> <script src="https://unpkg.com/reflect-metadata@0.1.13/Reflect.js"></script> <script src="https://unpkg.com/systemjs@0.21.3/dist/system.js"></script> <script src="config.js"></script> <script> System.import("app").catch(console.error.bind(console)); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <demo-app>Loading...</demo-app> </div> </body> </html>

If the default editor is unsuitable, you can replace it with a custom editor. For this, implement an editCellTemplate that allows you to configure the replacement editor's appearance and behavior. To change the cell value and, optionally, the displayed value after the editor's value is changed, use the setValue() method of the editCellTemplate. In this demo, the default editors in the Owner and Assignees columns are replaced with the DropDownBox and TagBox components.