Feel free to share demo-related thoughts here.
If you have technical questions, please create a support ticket in the DevExpress Support Center.
Thank you for the feedback!
If you have technical questions, please create a support ticket in the DevExpress Support Center.
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-pager [visible]="true"></dxo-pager>
<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/common/http': {
'esModule': true,
},
'@angular/compiler': {
'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.7/cjs',
'@devextreme/runtime': 'npm:@devextreme/runtime@3.0.13',
'devextreme/bundles/dx.all': 'npm:devextreme@24.1.7/bundles/dx.all.js',
'devextreme-quill': 'npm:devextreme-quill@1.7.1/dist/dx-quill.min.js',
'devexpress-diagram': 'npm:devexpress-diagram@2.2.13',
'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',
'devextreme-angular/http': 'bundles:devextreme-angular/devextreme-angular-http.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.13/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.7/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.3/bundles/zone.umd.min.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.