Reactive Forms

The Scheduler provides options for editing its events by using Reactive Angular Forms.

Basic Concepts

To modify a Scheduler event or add a new one:

  1. Call the addEvent or the editEvent method respectively.
  2. Pass the FormGroup property as one of its parameters. The FormGroup configuration is part of the Angular Forms package.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import {
    CancelEvent,
    CrudOperation,
    EditMode,
    EventClickEvent,
    RemoveEvent,
    SaveEvent,
    SchedulerComponent,
    SlotClickEvent
} from '@progress/kendo-angular-scheduler';
import '@progress/kendo-date-math/tz/regions/Europe';
import '@progress/kendo-date-math/tz/regions/NorthAmerica';
import { filter } from 'rxjs/operators';

import { EditService } from './edit.service';

@Component({
    selector: 'my-app',
    template: `
    <kendo-scheduler
        [kendoSchedulerBinding]="editService.events | async"
        [modelFields]="editService.fields"
        [loading]="editService.loading"
        [editable]="true"
        [selectedViewIndex]="0"
        [selectedDate]="selectedDate"
        (slotDblClick)="slotDblClickHandler($event)"
        (eventDblClick)="eventDblClickHandler($event)"
        (cancel)="cancelHandler($event)"
        (save)="saveHandler($event)"
        (remove)="removeHandler($event)"
        (dragEnd)="dragEndHandler($event)"
        (resizeEnd)="resizeEndHandler($event)"
        style="height: 600px;"
    >
        <kendo-scheduler-week-view startTime="07:00">
        </kendo-scheduler-week-view>

    </kendo-scheduler>
    `
})
export class AppComponent implements OnInit {
    public selectedDate: Date = new Date('2013-06-10T00:00:00');
    public formGroup: FormGroup;

    constructor(
        public formBuilder: FormBuilder,
        public editService: EditService
    ) { }

    public ngOnInit(): void {
        this.editService.read();
    }

    public slotDblClickHandler({ sender, start, end, isAllDay }: SlotClickEvent): void {
        this.closeEditor(sender);

        this.formGroup = this.formBuilder.group({
            'Start': [start, Validators.required],
            'End': [end, Validators.required],
            'StartTimezone': new FormControl(),
            'EndTimezone': new FormControl(),
            'IsAllDay': isAllDay,
            'Title': new FormControl(''),
            'Description': new FormControl(''),
            'RecurrenceRule': new FormControl(),
            'RecurrenceID': new FormControl()
        });

        sender.addEvent(this.formGroup);
    }

    public eventDblClickHandler({ sender, event }: EventClickEvent): void {
        this.closeEditor(sender);

        let dataItem = event.dataItem;
        if (this.editService.isRecurring(dataItem)) {
            sender.openRecurringConfirmationDialog(CrudOperation.Edit)
                // The result will be undefined if the dialog was closed.
                .pipe(filter(editMode => editMode !== undefined))
                .subscribe((editMode: EditMode) => {
                    if (editMode === EditMode.Series) {
                        dataItem = this.editService.findRecurrenceMaster(dataItem);
                    }
                    this.formGroup = this.createFormGroup(dataItem);
                    sender.editEvent(dataItem, { group: this.formGroup, mode: editMode });
                });
        } else {
            this.formGroup = this.createFormGroup(dataItem);
            sender.editEvent(dataItem, { group: this.formGroup });
        }
    }

    public createFormGroup(dataItem: any): FormGroup {
        return this.formBuilder.group({
            'Start': [dataItem.Start, Validators.required],
            'End': [dataItem.End, Validators.required],
            'StartTimezone': [dataItem.StartTimezone],
            'EndTimezone': [dataItem.EndTimezone],
            'IsAllDay': dataItem.IsAllDay,
            'Title': dataItem.Title,
            'Description': dataItem.Description,
            'RecurrenceRule': dataItem.RecurrenceRule,
            'RecurrenceID': dataItem.RecurrenceID
        });
    }

    public cancelHandler({ sender }: CancelEvent): void {
        this.closeEditor(sender);
    }

    public removeHandler({ sender, dataItem }: RemoveEvent): void {
        if (this.editService.isRecurring(dataItem)) {
            sender.openRecurringConfirmationDialog(CrudOperation.Remove)
                  // The result will be undefined if the dialog was closed.
                  .pipe(filter(editMode => editMode !== undefined))
                  .subscribe((editMode) => {
                      this.handleRemove(dataItem, editMode);
                  });
        } else {
            sender.openRemoveConfirmationDialog().subscribe((shouldRemove) => {
                if (shouldRemove) {
                    this.editService.remove(dataItem);
                }
            });
        }
    }

    public saveHandler({ sender, formGroup, isNew, dataItem, mode }: SaveEvent): void {
        if (formGroup.valid) {
            const formValue = formGroup.value;

            if (isNew) {
                this.editService.create(formValue);
            } else {
                this.handleUpdate(dataItem, formValue, mode);
            }

            this.closeEditor(sender);
        }
    }

    public dragEndHandler({ sender, event, start, end, isAllDay }): void {
        let value = { Start: start, End: end, IsAllDay: isAllDay };
        let dataItem = event.dataItem;

        if (this.editService.isRecurring(dataItem)) {
            sender.openRecurringConfirmationDialog(CrudOperation.Edit)
                .pipe(filter(editMode => editMode !== undefined))
                .subscribe((editMode: EditMode) => {
                    if (editMode === EditMode.Series) {
                        dataItem = this.editService.findRecurrenceMaster(dataItem);
                        value.Start = this.seriesDate(dataItem.Start, event.dataItem.Start, start);
                        value.End = this.seriesDate(dataItem.End, event.dataItem.End, end);
                    } else {
                        value = { ...dataItem, ...value };
                    }

                    this.handleUpdate(dataItem, value, editMode);
                });
        } else {
            this.handleUpdate(dataItem, value);
        }
    }

    public resizeEndHandler({ sender, event, start, end }): void {
        let value = { Start: start, End: end };
        let dataItem = event.dataItem;

        if (this.editService.isRecurring(dataItem)) {
            sender.openRecurringConfirmationDialog(CrudOperation.Edit)
                .pipe(filter(editMode => editMode !== undefined))
                .subscribe((editMode: EditMode) => {
                    if (editMode === EditMode.Series) {
                        dataItem = this.editService.findRecurrenceMaster(dataItem);
                        value.Start = this.seriesDate(dataItem.Start, event.dataItem.Start, start);
                        value.End = this.seriesDate(dataItem.End, event.dataItem.End, end);
                    } else {
                        value = { ...dataItem, ...value };
                    }

                    this.handleUpdate(dataItem, value, editMode);
                });
        } else {
            this.handleUpdate(dataItem, value);
        }
    }

    private closeEditor(scheduler: SchedulerComponent): void {
        scheduler.closeEvent();

        this.formGroup = undefined;
    }

    private handleUpdate(item: any, value: any, mode?: EditMode): void {
        const service = this.editService;
        if (mode === EditMode.Occurrence) {
            if (service.isException(item)) {
                service.update(item, value);
            } else {
                service.createException(item, value);
            }
        } else {
            // The item is non-recurring or we are editing the entire series.
            service.update(item, value);
        }
    }

    private handleRemove(item: any, mode: EditMode): void {
        const service = this.editService;
        if (mode === EditMode.Series) {
            service.removeSeries(item);
        } else if (mode === EditMode.Occurrence) {
            if (service.isException(item)) {
                service.remove(item);
            } else {
                service.removeOccurrence(item);
            }
        } else {
            service.remove(item);
        }
    }

    private seriesDate(head: Date, occurence: Date, current: Date): Date {
        const year = occurence.getFullYear() === current.getFullYear() ? head.getFullYear() : current.getFullYear();
        const month = occurence.getMonth() === current.getMonth() ? head.getMonth() : current.getMonth();
        const date = occurence.getDate() === current.getDate() ? head.getDate() : current.getDate();
        const hours = occurence.getHours() === current.getHours() ? head.getHours() : current.getHours();
        const minutes = occurence.getMinutes() === current.getMinutes() ? head.getMinutes() : current.getMinutes();

        return new Date(year, month, date, hours, minutes);
    }
}
import { HttpClientJsonpModule, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SchedulerModule } from '@progress/kendo-angular-scheduler';

import { AppComponent } from './app.component';
import { EditService } from './edit.service';

@NgModule({
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        SchedulerModule,
        HttpClientModule,
        HttpClientJsonpModule
    ],
    declarations: [ AppComponent ],
    bootstrap: [ AppComponent ],
    providers: [ EditService ]
})

export class AppModule { }

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable, zip } from 'rxjs';
import { map, tap } from 'rxjs/operators';

import { BaseEditService, SchedulerModelFields } from '@progress/kendo-angular-scheduler';
import { parseDate } from '@progress/kendo-angular-intl';

import { MyEvent } from './my-event.interface';

const CREATE_ACTION = 'create';
const UPDATE_ACTION = 'update';
const REMOVE_ACTION = 'destroy';

const fields: SchedulerModelFields = {
    id: 'TaskID',
    title: 'Title',
    description: 'Description',
    startTimezone: 'StartTimezone',
    start: 'Start',
    end: 'End',
    endTimezone: 'EndTimezone',
    isAllDay: 'IsAllDay',
    recurrenceRule: 'RecurrenceRule',
    recurrenceId: 'RecurrenceID',
    recurrenceExceptions: 'RecurrenceException'
};

@Injectable()
export class EditService extends BaseEditService<MyEvent> {
    public loading = false;

    constructor(private http: HttpClient) {
        super(fields);
    }

    public read(): void {
        if (this.data.length) {
            this.source.next(this.data);
            return;
        }

        this.fetch().subscribe(data => {
            this.data = data.map(item => this.readEvent(item));
            this.source.next(this.data);
        });
    }

    protected save(created: MyEvent[], updated: MyEvent[], deleted: MyEvent[]): void {
        const completed = [];
        if (deleted.length) {
            completed.push(this.fetch(REMOVE_ACTION, deleted));
        }

        if (updated.length) {
            completed.push(this.fetch(UPDATE_ACTION, updated));
        }

        if (created.length) {
            completed.push(this.fetch(CREATE_ACTION, created));
        }

        zip(...completed).subscribe(() => this.read());
    }

    protected fetch(action: string = '', data?: any): Observable<any[]> {
        this.loading = true;

        return this.http
            .jsonp(`https://demos.telerik.com/kendo-ui/service/tasks/${action}?${this.serializeModels(data)}`, 'callback')
            .pipe(
                map(res => <any[]>res),
                tap(() => this.loading = false)
            );
    }

    private readEvent(item: any): MyEvent {
        return {
            ...item,
            Start: parseDate(item.Start),
            End: parseDate(item.End),
            RecurrenceException: this.parseExceptions(item.RecurrenceException)
        };
    }

    private serializeModels(events: MyEvent[]): string {
        if (!events) {
            return '';
        }

        const data = events.map(event => ({
             ...event,
             RecurrenceException:
                this.serializeExceptions(event.RecurrenceException)
        }));

        return `&models=${JSON.stringify(data)}`;
    }
}

export interface MyEvent {
    TaskID?: number;
    OwnerID?: number;
    Title?: string;
    Description?: string;
    Start?: Date;
    End?: Date;
    StartTimezone?: string;
    EndTimezone?: string;
    IsAllDay?: boolean;
    RecurrenceException?: any;
    RecurrenceID?: number;
    RecurrenceRule?: string;
}
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Setup

To enable the editing mode of the Scheduler in Angular Reactive Forms, attach handlers for the corresponding CRUD data operations.

  1. When the user click or double-clicks an empty slot in the Scheduler, the component emits the slotClick and slotDblClick events. To initiate the create action, utilize either of those events.

    <kendo-scheduler
       [kendoSchedulerBinding]="events"
       (slotDblClick)="onSlotDblClick($event)"
    >
    <!-- the rest of the configuration -->
    </kendo-scheduler>
  2. When the user clicks or double-clicks an event in the Scheduler, the component emits the eventClick and eventDblClick events. To start editing the event in the Scheduler, utilize either of those events and then initiate the update action.

    <kendo-scheduler
       [kendoSchedulerBinding]="events"
       (slotDblClick)="onSlotDblClick($event)"
       (eventDblClick)="onEventDblClick($event)"
    >
    <!-- the rest of the configuration -->
    </kendo-scheduler>
  3. Attach handlers for the save, cancel, and remove events.

    <kendo-scheduler
       [kendoSchedulerBinding]="events"
       (slotDblClick)="onSlotDblClick($event)"
       (eventDblClick)="onEventDblClick($event)"
       (cancel)="cancelHandler($event)"
       (save)="saveHandler($event)"
       (remove)="removeHandler($event)"
    >
    <!-- the rest of the configuration -->
    </kendo-scheduler>

Edit Operations

The Scheduler provides the following specific edit operations for its displayed events:

Editing Existing Events

The FormGroup configuration describes the view model for that editor.

The EditMode contains information about the current editing operation. The supported values are:

  • Series—Used when the user edits a recurring event instance and the update applies to all events in the series.
  • Occurrence—Used when the user edits a recurring event instance and the update applies to this instance only.
  • Event—Used by default when the user edits a non-recurring event.

To enable the edit mode of an event in the Scheduler, use either of the following approaches:

  • Inside the corresponding event handler (for example, eventDblClick), pass the event to the editEvent method and provide the appropriate editing options as demonstrated in the following example.

    public eventDblClickHandler({ sender, event }): void {
       this.closeEditor(sender);
    
       // Define all event fields, validators, and default values.
       this.formGroup = this.formBuilder.group({
           'TaskID': event.id,
           'Start': [event.start, Validators.required],
           'End': [event.end, Validators.required],
           'StartTimezone': [event.startTimezone],
           'EndTimezone': [event.endTimezone],
           'IsAllDay': event.isAllDay,
           'Title': event.title,
           'Description': event.description,
           'RecurrenceRule': event.recurrenceRule,
           'RecurrenceID': event.recurrenceId
       });
    
       sender.editEvent(event, { group: this.formGroup, mode: <Edit Mode> });
    }
  • Enable the edit mode of the Scheduler by utilizing its openRecurringConfirmationDialog method.

    public eventDblClickHandler({ sender, event }: SlotClickEvent): void {
       this.closeEditor(sender);
    
       let dataItem = event.dataItem;
       if (this.editService.isRecurring(dataItem)) {
           sender.openRecurringConfirmationDialog(CrudOperation.Edit)
               // The result will be undefined if the dialog was closed.
               .pipe(filter(editMode => editMode !== undefined))
               .subscribe((editMode: EditMode) => {
                   if (editMode === EditMode.Series) {
                       dataItem = this.editService.findRecurrenceMaster(dataItem);
                   }
    
                   this.formGroup = this.formBuilder.group({
                       <!-- FormGroup Confguration -->
                   });
    
                   sender.editEvent(dataItem, { group: this.formGroup, mode: editMode });
               });
       } else {
           this.formGroup = this.formBuilder.group({
               <!-- FormGroup Confguration -->
           });
    
           sender.editEvent(dataItem, { group: this.formGroup });
       }
    }

Adding New Events

To show the edit dialog for the new event, call the addEvent method inside the corresponding event handler (for example, slotDblClick) and provide the FormGroup configuration which describes the view model. You will receive the corresponding information for the clicked slot as an event argument—for example, start, end, or isAllDay.

public slotDblClickHandler({ sender, start, end, isAllDay }: EventClickEvent): void {
    this.closeEditor(sender);

    this.formGroup = this.formBuilder.group({
        'Start': [start, Validators.required],
        'End': [end, Validators.required],
        'StartTimezone': new FormControl(),
        'EndTimezone': new FormControl(),
        'IsAllDay': isAllDay,
        'Title': new FormControl(''),
        'Description': new FormControl(''),
        'RecurrenceRule': new FormControl(),
        'RecurrenceID': new FormControl()
    });

    sender.addEvent(this.formGroup);
}

Saving Events

When the user clicks the Save action of the edit dialog, the Scheduler fires its save event.

When an event is saved (updated), the Scheduler provides the following possible options:

  • Updating the event depending on the currently selected edit mode.
  • Switching the current event back to its view mode through its closeEvent method.

    public saveHandler({ sender, formGroup, isNew, dataItem, mode }: SaveEvent): void {
       if (formGroup.valid) {
           const formValue = formGroup.value;
    
           if (isNew) {
               this.editService.create(formValue);
           } else {
               this.handleUpdate(dataItem, formValue, mode);
           }
    
           this.closeEditor(sender);
       }
    }

The saving of events can result in the following actions:

  • An update of a non-recurring item.
  • An update of a recurring item along with all items in the series.
  • An update of a single instance of recurring items, that is, creating an exception.
  • An update of an already existing exception.
private handleUpdate(item: any, value: any, mode: EditMode): void {
  const service = this.editService;
  if (mode === EditMode.Occurrence) {
    if (service.isException(item)) {
      service.update(item, value);
      } else {
        service.createException(item, value);
      }
      } else {
        // The item is non-recurring or we are editing the entire series.
        service.update(item, value);
      }
    }

Removing Existing Events

When the user clicks the Remove icon of a displayed event, the Scheduler fires its remove event.

Enable the edit mode of the Scheduler during the removal of the event by utilizing its openRecurringConfirmationDialog or openRemoveConfirmationDialog method inside the event handler.

public removeHandler({ sender, dataItem }: RemoveEvent): void {
    if (this.editService.isRecurring(dataItem)) {
        sender.openRecurringConfirmationDialog(CrudOperation.Remove)
                // The result will be undefined if the dialog was closed.
                .pipe(filter(editMode => editMode !== undefined))
                .subscribe((editMode) => {
                    this.handleRemove(dataItem, editMode);
                });
    } else {
        sender.openRemoveConfirmationDialog().subscribe((shouldRemove) => {
            if (shouldRemove) {
                this.editService.remove(dataItem);
            }
        });
    }
}

The removal of events can result in the following operations:

  • A removal of a non-recurring item.
  • A removal of a recurring item along with all items in the series.
  • A removal of a single instance of recurring items, that is, creating an exception.
  • A removal of an already existing exception.
private handleRemove(item: any, mode: EditMode): void {
    const service = this.editService;
    if (mode === EditMode.Series) {
        service.removeSeries(item);
    } else if (mode === EditMode.Occurrence) {
        if (service.isException(item)) {
            service.remove(item);
        } else {
            service.removeOccurrence(item);
        }
    } else {
        service.remove(item);
    }
}

Cancelling Event Editing

When the user clicks the Cancel action of the edit dialog, the Scheduler fires the cancel event. Inside the event handler, switch the event back to view mode by calling the closeEvent method.

public cancelHandler({ sender }): void {
    scheduler.closeEvent();

    this.formGroup = undefined;
}

Dragging Events

The Scheduler provides options for editing its events by dragging them to another date or time slot. To update the events after they are dragged by the user, handle the dragEnd event of the Scheduler. The event argument contains the start and end date, the isAllDay value, and the resources as updated after dragging.

To disable the dragging of events, use the editable and drag options.

public dragEndHandler({ sender, event, start, end, isAllDay }): void {
       const value = { Start: start, End: end, IsAllDay: isAllDay };
       let dataItem = event.dataItem;

       if (this.editService.isRecurring(dataItem)) {
           sender.openRecurringConfirmationDialog(CrudOperation.Edit)
               .pipe(filter(editMode => editMode !== undefined))
               .subscribe((editMode: EditMode) => {
                   if (editMode === EditMode.Series) {
                       dataItem = this.editService.findRecurrenceMaster(dataItem);

                       // apply only the changes that were made to the occurence during dragging to avoid overriding the recurrence head date
                       value.Start = this.seriesDate(dataItem.Start, event.dataItem.Start, start);
                       value.End = this.seriesDate(dataItem.End, event.dataItem.End, end);
                   } else {
                       // create exception
                       value = { ...dataItem, ...value };
                   }

                   this.handleUpdate(dataItem, value, editMode);
               });
       } else {
           this.handleUpdate(dataItem, value);
       }
   }

   private seriesDate(head: Date, occurence: Date, current: Date): Date {
       const year = occurence.getFullYear() === current.getFullYear() ? head.getFullYear() : current.getFullYear();
       const month = occurence.getMonth() === current.getMonth() ? head.getMonth() : current.getMonth();
       const date = occurence.getDate() === current.getDate() ? head.getDate() : current.getDate();
       const hours = occurence.getHours() === current.getHours() ? head.getHours() : current.getHours();
       const minutes = occurence.getMinutes() === current.getMinutes() ? head.getMinutes() : current.getMinutes();

       return new Date(year, month, date, hours, minutes);
   }

Resizing Events

The Scheduler provides options for changing the start and end date of an event by dragging its resize handles. To update the events after they are resized by the user, handle the resizeEnd event of the Scheduler. The event argument contains the start and end date as updated after resizing.

To disable the resizing of events, use the editable and resize options.

public resizeEndHandler({ sender, event, start, end }): void {
       const value = { Start: start, End: end };
       let dataItem = event.dataItem;

       if (this.editService.isRecurring(dataItem)) {
           sender.openRecurringConfirmationDialog(CrudOperation.Edit)
               .pipe(filter(editMode => editMode !== undefined))
               .subscribe((editMode: EditMode) => {
                   if (editMode === EditMode.Series) {
                       dataItem = this.editService.findRecurrenceMaster(dataItem);

                       // apply only the changes that were made to the occurence during resizing to avoid overriding the recurrence head date
                       value.Start = this.seriesDate(dataItem.Start, event.dataItem.Start, start);
                       value.End = this.seriesDate(dataItem.End, event.dataItem.End, end);
                   } else {
                       // create exception
                       value = { ...dataItem, ...value };
                   }

                   this.handleUpdate(dataItem, value, editMode);
               });
       } else {
           this.handleUpdate(dataItem, value);
       }
   }

   private seriesDate(head: Date, occurence: Date, current: Date): Date {
       const year = occurence.getFullYear() === current.getFullYear() ? head.getFullYear() : current.getFullYear();
       const month = occurence.getMonth() === current.getMonth() ? head.getMonth() : current.getMonth();
       const date = occurence.getDate() === current.getDate() ? head.getDate() : current.getDate();
       const hours = occurence.getHours() === current.getHours() ? head.getHours() : current.getHours();
       const minutes = occurence.getMinutes() === current.getMinutes() ? head.getMinutes() : current.getMinutes();

       return new Date(year, month, date, hours, minutes);
   }

In this article