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
x
<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]="getSelectedRowKeys(cellInfo.value)"
[focusedRowEnabled]="true"
[focusedRowKey]="cellInfo.value"
[hoverStateEnabled]="true"
(onContextMenuPreparing)="$event.items = []"
(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>
xxxxxxxxxx
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();
}
let modulePrefix = '';
// @ts-ignore
if (window && window.config?.packageConfigPaths) {
modulePrefix = '/app';
}
@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/NetCore/api/DataGridCustomEditors';
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 };
},
});
}
getSelectedRowKeys<T>(value: T): T[] {
return value !== null && value !== undefined ? [value] : [];
}
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);
xxxxxxxxxx
.status-icon {
height: 16px;
width: 16px;
display: inline-block;
margin-right: 8px;
}
.middle {
vertical-align: middle;
}
xxxxxxxxxx
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;
}
}
xxxxxxxxxx
// 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',
'chat',
'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',
'pagination',
'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/animations': {
'esModule': true,
},
'@angular/forms': {
'esModule': true,
},
'openai': {
'esModule': true,
},
},
paths: {
'npm:': 'https://unpkg.com/',
'bundles:': 'bundles/',
'externals:': 'bundles/externals/',
},
map: {
'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js',
'typescript': 'npm:typescript@4.2.4/lib/typescript.js',
'jszip': 'npm:jszip@3.10.1/dist/jszip.min.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.2.6/cjs',
'@devextreme/runtime': 'npm:@devextreme/runtime@3.0.12',
'devextreme/bundles/dx.all': 'npm:devextreme@24.2.6/bundles/dx.all.js',
'devextreme-quill': 'npm:devextreme-quill@1.7.1/dist/dx-quill.min.js',
'devexpress-diagram': 'npm:devexpress-diagram@2.2.15',
'devexpress-gantt': 'npm:devexpress-gantt@4.1.60',
/* 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`;
acc[`devextreme-angular/ui/${name}/nested`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}-nested.umd.js`;
return acc;
}, {}),
'tslib': 'npm:tslib/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@5.0.0/index.js',
'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js',
'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js',
'es6-object-assign': 'npm:es6-object-assign',
'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/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/common/core/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.12/inferno/package.json',
'npm:rxjs@7.5.3/package.json',
'npm:rxjs@7.5.3/operators/package.json',
'npm:devexpress-diagram@2.2.15/package.json',
'npm:devexpress-gantt@4.1.60/package.json',
],
};
System.config(window.config);
// System.import('@angular/compiler').catch(console.error.bind(console));
xxxxxxxxxx
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head></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.