DevExtreme v26.1 is now available.

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

Your search did not match any results.

Angular Scheduler - Resolve Time Conflicts

This example addresses appointment time conflicts when using the DevExtreme Scheduler. Use the Allow Overlapping Appointments select-box to select a desired time conflict resolution mode.

Backend API
<dx-scheduler [dataSource]="appointmentsData" [views]="views" currentView="week" [startDayHour]="9" [endDayHour]="19" [currentDate]="currentDate" [height]="600" [showAllDayPanel]="false" allDayPanelMode="hidden" (onAppointmentAdding)="onAppointmentAdding($event)" (onAppointmentUpdating)="onAppointmentUpdating($event)" > <dxi-scheduler-resource fieldExpr="assigneeId" [dataSource]="assignees" valueExpr="id" colorExpr="color" icon="user" [allowMultiple]="true" ></dxi-scheduler-resource> <dxo-scheduler-editing [popup]="popupOptions"> <dxo-scheduler-form labelMode="hidden" [elementAttr]="formElementAttr" [customizeItem]="customizeItem" [onInitialized]="onFormInitialized" > <dxi-scheduler-item name="conflictInformer" template="conflictInformerTemplate" ></dxi-scheduler-item> <dxi-scheduler-item name="mainGroup" itemType="group"> <dxi-scheduler-item name="subjectGroup"></dxi-scheduler-item> <dxi-scheduler-item name="dateGroup"></dxi-scheduler-item> <dxi-scheduler-item name="repeatGroup"></dxi-scheduler-item> <dxi-scheduler-item name="assigneeIdGroup"> <dxi-scheduler-item name="assigneeIdIcon"></dxi-scheduler-item> <dxi-scheduler-item name="assigneeIdEditor" [isRequired]="true" [editorOptions]="assigneeIdEditorOptions" ></dxi-scheduler-item> </dxi-scheduler-item> </dxi-scheduler-item> <dxi-scheduler-item name="recurrenceGroup" itemType="group" ></dxi-scheduler-item> </dxo-scheduler-form> </dxo-scheduler-editing> <div *dxTemplate="let _ of 'conflictInformerTemplate'"> <div class="conflict-informer" >This time slot conflicts with another appointment.</div > </div> <div *dxTemplate="let tagData of 'assigneeTagTemplate'"> <div class="dx-tag-content" [style.background-color]="tagData.color" [style.border-color]="tagData.color" > <span>{{ tagData.text }}</span> <div class="dx-tag-remove-button"></div> </div> </div> </dx-scheduler> <div class="options"> <div class="option"> <span>Allow Overlapping Appointments</span> <dx-select-box [items]="overlappingRuleItems" valueExpr="value" displayExpr="text" value="sameResource" (onValueChanged)="onOverlappingRuleChanged($event)" ></dx-select-box> </div> </div>
import { bootstrapApplication } from '@angular/platform-browser'; import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; import { DxSchedulerModule, DxSelectBoxModule, DxTemplateModule } from 'devextreme-angular'; import { DxSchedulerTypes } from 'devextreme-angular/ui/scheduler'; import { DxSelectBoxTypes } from 'devextreme-angular/ui/select-box'; import { DxFormTypes } from 'devextreme-angular/ui/form'; import { DxPopupTypes } from 'devextreme-angular/ui/popup'; import { DxTagBoxTypes } from 'devextreme-angular/ui/tag-box'; import { custom as customDialog } from 'devextreme/ui/dialog'; import { Appointment, Assignee, Service, assignees } from './app.service'; type dxScheduler = NonNullable<DxSchedulerTypes.InitializedEvent['component']>; type dxForm = NonNullable<DxFormTypes.InitializedEvent['component']>; type dxPopup = NonNullable<DxPopupTypes.InitializedEvent['component']>; 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], preserveWhitespaces: true, imports: [ DxSchedulerModule, DxSelectBoxModule, DxTemplateModule, ], }) export class AppComponent { appointmentsData: Appointment[]; currentDate: Date = new Date(2026, 1, 10); views: DxSchedulerTypes.ViewType[] = ['day', 'week', 'workWeek', 'month']; assignees: Assignee[] = assignees; overlappingRuleItems = [ { value: 'sameResource', text: 'Different Resources' }, { value: 'allResources', text: 'Never' }, ]; assigneeIdEditorOptions = { onValueChanged: (e: DxTagBoxTypes.ValueChangedEvent) => { if (e.value?.length > 1) { e.component.option('value', [e.value[e.value.length - 1]]); } }, tagTemplate: 'assigneeTagTemplate', }; popupOptions = { onInitialized: (e: DxPopupTypes.InitializedEvent) => { this.popup = e.component; }, onHidden: () => { this.setConflictError(false); }, }; formElementAttr = { id: 'form', class: 'hide-informer' }; private popup?: dxPopup; private form?: dxForm; private overlappingRule = 'sameResource'; constructor(service: Service) { this.appointmentsData = service.getAppointments(); } private showConflictError = false; setConflictError(show: boolean): void { this.showConflictError = show; this.form?.option('elementAttr.class', show ? '' : 'hide-informer'); } onFormInitialized = (e: DxFormTypes.InitializedEvent): void => { this.form = e.component; this.form?.on('fieldDataChanged', (event: { dataField: string }) => { if (this.showConflictError && ['startDate', 'endDate', 'assigneeId', 'recurrenceRule'].includes(event.dataField)) { this.setConflictError(false); this.form?.validate(); } }); }; customizeItem = (item: DxFormTypes.SimpleItem): void => { if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') { item.label.visible = true; } else if (item.name === 'subjectEditor') { item.editorOptions = item.editorOptions || {}; item.editorOptions.placeholder = 'Add title'; } if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') { item.validationRules = [ { type: 'required' }, { type: 'custom', message: 'Time conflict', ignoreEmptyValue: true, reevaluate: true, validationCallback: () => !this.showConflictError, }, ]; } }; private getNextDay(date: Date): Date { const next = new Date(date); next.setDate(next.getDate() + 1); return next; } private getEndDate(occurrence: DxSchedulerTypes.Occurrence): Date { return (occurrence.appointmentData as Appointment).allDay ? this.getNextDay(occurrence.startDate) : occurrence.endDate; } private isOverlapping(a: DxSchedulerTypes.Occurrence, b: DxSchedulerTypes.Occurrence): boolean { const aEnd = this.getEndDate(a); const bEnd = this.getEndDate(b); if (a.startDate >= bEnd || b.startDate >= aEnd) return false; if (this.overlappingRule === 'sameResource') { return (a.appointmentData as Appointment).assigneeId[0] === (b.appointmentData as Appointment).assigneeId[0]; } return true; } private detectConflict(scheduler: dxScheduler, newAppointment: Appointment): boolean { const allAppointments = scheduler.getDataSource().items() as Appointment[]; const startDate = new Date(newAppointment.startDate); let endDate: Date; if (newAppointment.recurrenceRule) { endDate = scheduler.getEndViewDate(); } else if (newAppointment.allDay) { endDate = this.getNextDay(startDate); } else { endDate = new Date(newAppointment.endDate); } const existingOccurrences = scheduler .getOccurrences(startDate, endDate, allAppointments) .filter((occurrence) => (occurrence.appointmentData as Appointment).id !== newAppointment.id); const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]); return newOccurrences.some((newOccurrence) => existingOccurrences.some((existingOccurrence) => this.isOverlapping(newOccurrence, existingOccurrence), ), ); } private handleConflict( e: DxSchedulerTypes.AppointmentAddingEvent | DxSchedulerTypes.AppointmentUpdatingEvent, appointmentData: Appointment, ): void { if (!this.detectConflict(e.component, appointmentData)) { this.setConflictError(false); return; } e.cancel = true; if (this.popup?.option('visible')) { this.setConflictError(true); this.form?.validate(); } else { const dialog = customDialog({ showTitle: false, messageHtml: '<p id="conflict-dialog">This time slot conflicts with another appointment.</p>', buttons: [{ type: 'default', text: 'Close', stylingMode: 'contained', onClick: () => { dialog.hide(); }, }], }); dialog.show(); } } onAppointmentAdding(e: DxSchedulerTypes.AppointmentAddingEvent): void { this.handleConflict(e, e.appointmentData as Appointment); } onAppointmentUpdating(e: DxSchedulerTypes.AppointmentUpdatingEvent): void { this.handleConflict(e, { ...e.oldData, ...e.newData } as Appointment); } onOverlappingRuleChanged(e: DxSelectBoxTypes.ValueChangedEvent): void { this.overlappingRule = e.value; } } bootstrapApplication(AppComponent, { providers: [ provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), ], });
::ng-deep .dx-scheduler-appointment { color: #242424; } .options { padding: 20px; background-color: rgba(191, 191, 191, 0.15); margin-top: 20px; } .option { margin-top: 10px; display: flex; align-items: center; gap: 10px; } ::ng-deep .hide-informer .dx-item:has(.conflict-informer) { display: none !important; } ::ng-deep .conflict-informer { background-color: #FCEAE8; color: #C50F1F; font-size: 12px; padding: 0 12px; height: 36px; line-height: 36px; box-sizing: border-box; margin-bottom: 8px; } ::ng-deep .dx-dialog:has(#conflict-dialog) .dx-overlay-content { width: 280px; } ::ng-deep .dx-dialog:has(#conflict-dialog) .dx-dialog-content { padding-bottom: 16px; } ::ng-deep .dx-dialog:has(#conflict-dialog) .dx-dialog-buttons { padding-top: 0; padding-bottom: 16px; } ::ng-deep .dx-dialog:has(#conflict-dialog) .dx-toolbar-center, ::ng-deep .dx-dialog:has(#conflict-dialog) .dx-button { width: 100%; }
import { Injectable } from '@angular/core'; export interface Appointment { id: number; text: string; startDate: Date; endDate: Date; assigneeId: number[]; recurrenceRule?: string; allDay?: boolean; } export interface Assignee { id: number; text: string; color: string; } export const assignees: Assignee[] = [ { id: 1, text: 'Samantha Bright', color: '#A7E3A5' }, { id: 2, text: 'John Heart', color: '#CFE4FA' }, { id: 3, text: 'Todd Hoffman', color: '#F9E2AE' }, { id: 4, text: 'Sandra Johnson', color: '#F1BBBC' }, ]; const appointments: Appointment[] = [ { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), assigneeId: [2], }, { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), assigneeId: [3], }, { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), assigneeId: [1], }, { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), assigneeId: [1], }, { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), assigneeId: [4], }, { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), assigneeId: [2], }, { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), assigneeId: [3], }, { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), assigneeId: [2], }, { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), assigneeId: [1], }, { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), assigneeId: [4], }, { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), assigneeId: [2], }, ]; @Injectable() export class Service { getAppointments(): Appointment[] { return appointments; } }
// 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', 'card-view', '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', 'speech-to-text', 'speed-dial-action', 'splitter', 'stepper', '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/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, }, 'zod': { 'esModule': true, }, 'zod-to-json-schema': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', 'anti-forgery:': '../../../../shared/anti-forgery/', }, map: { 'anti-forgery': 'anti-forgery:fetch-override.js', '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/core/primitives/di': 'bundles:@angular/core.primitives.di.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@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.9/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.29', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.69', /* devextreme-angular umd maps */ 'devextreme-angular': 'bundles:devextreme-angular/devextreme-angular.umd.js', 'devextreme-angular/common/ai-integration': 'bundles:devextreme-angular/devextreme-angular-common-ai-integration.umd.js', 'devextreme-angular/core': 'bundles:devextreme-angular/devextreme-angular-core.umd.js', 'devextreme-angular/common/charts': 'bundles:devextreme-angular/devextreme-angular-common-charts.umd.js', 'devextreme-angular/common/core/animation': 'bundles:devextreme-angular/devextreme-angular-common-core-animation.umd.js', 'devextreme-angular/common/core/environment': 'bundles:devextreme-angular/devextreme-angular-common-core-environment.umd.js', 'devextreme-angular/common/core/events': 'bundles:devextreme-angular/devextreme-angular-common-core-events.umd.js', 'devextreme-angular/common/core/localization': 'bundles:devextreme-angular/devextreme-angular-common-core-localization.umd.js', 'devextreme-angular/common/core': 'bundles:devextreme-angular/devextreme-angular-common-core.umd.js', 'devextreme-angular/common/data/custom-store': 'bundles:devextreme-angular/devextreme-angular-common-data-custom-store.umd.js', 'devextreme-angular/common/data': 'bundles:devextreme-angular/devextreme-angular-common-data.umd.js', 'devextreme-angular/common/export/excel': 'bundles:devextreme-angular/devextreme-angular-common-export-excel.umd.js', 'devextreme-angular/common/export/pdf': 'bundles:devextreme-angular/devextreme-angular-common-export-pdf.umd.js', 'devextreme-angular/common/export': 'bundles:devextreme-angular/devextreme-angular-common-export.umd.js', 'devextreme-angular/common/grids': 'bundles:devextreme-angular/devextreme-angular-common-grids.umd.js', 'devextreme-angular/common': 'bundles:devextreme-angular/devextreme-angular-common.umd.js', 'devextreme-angular/http': 'bundles:devextreme-angular/devextreme-angular-http.umd.js', 'devextreme-angular/core/tokens': 'bundles:devextreme-angular/devextreme-angular-core-tokens.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; }, {}), 'zod': 'externals:zod.bundle.js', 'zod-to-json-schema': 'externals:zod-to-json-schema.bundle.js', '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', '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@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/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', '@preact/signals-core': 'npm:@preact/signals-core@1.14.1/dist/signals-core.min.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', 'zone.js': 'npm:zone.js@0.15.1/bundles/zone.umd.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:rxjs@7.5.3/package.json', 'npm:rxjs@7.5.3/operators/package.json', 'npm:devexpress-diagram@2.2.29/package.json', 'npm:devexpress-gantt@4.1.69/package.json', ], }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line no-console // System.import('@angular/compiler').catch(console.error.bind(console)); // eslint-disable-next-line const useTgzInCSB = ['openai']; let packagesInfo = { "zod": { "version": "3.24.4" }, "zod-to-json-schema": { "version": "3.24.6" }, "@angular/core": { "version": "21.2.17" }, "core-js": { "version": "2.6.12" }, "typescript": { "version": "5.9.3" }, "zone.js": { "version": "0.15.1" } };
<!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/26.1.3/css/dx.light.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/zone.js@0.15.1/bundles/zone.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/reflect-metadata@0.1.13/Reflect.js"></script> <script src="https://cdn.jsdelivr.net/npm/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>

Detect Conflicts

Handle the onAppointmentAdding and onAppointmentUpdating events to check if a new or updated appointment creates a time conflict. Set e.cancel = true to block the operation when necessary.

Call getOccurrences to expand recurring appointments into individual occurrences within the target range. Check for overlapping time range values.

Conflict Detection Modes

The demo implements the following detection modes:

  • Different Resources: appointments assigned to different resources (assignees) can overlap.
  • Never: overlapping appointments are not allowed, regardless of resource assignment.

To implement resource-aware checks, access appointments and compare their assigneeId field values.

Display Errors

When a conflict is detected, the demo displays the error as follows:

  • A message box.
  • An inline validation message (if an appointment edit form is active).

To display inline validation, configure a custom form item inside editing.form and use the customizeItem function to attach custom validationRules to time editors.