Features

This tutorial shows how to manage resources (displayed as rows) using the built-in UI elements.

  • Angular 2 Scheduler component that displays a timeline for multiple resources
  • Loading resources from the server-side backend
  • Row header context menu
  • Context menu hint (active area icon)
  • Creating top-level resources (groups) using an inline edit box
  • Creating child resources using a context menu and modal dialog
  • Deleting resources using context menu
  • Moving resources using drag and drop
  • Includes a trial version of DayPilot Pro for JavaScript (see License below)
  • Use Angular CLI 1.0.0-beta.28.3

This tutorial focuses on managing resources (rows). For an introduction to the Scheduler component please see the basic tutorial:

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.

Live Demo

You can test this project in a live demo:

Loading Resources

angular2-scheduler-resource-management-loading.png

The Scheduler displays resources on the vertical axis. You can specify the resources using the "config" object ("resources" property). We will use a dummy DataService class to simulate a server-side backend. The resources are loaded using DataService in ngAfterViewInit() method:

this.ds.getResources().subscribe(result => this.config.resources = result);

By default the Scheduler displays resources in a flat list. We want to uses a hierarchy of resources - that needs to be enabled using "treeEnabled" property of the config:

  config: any = {
    treeEnabled: true,
  };

Simplified version of SchedulerComponent that loads resources and events from the data source:

scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
import {EditComponent} from "./edit.component";
import {CreateComponent} from "./create.component";

@Component({
  selector: 'scheduler-component',
  template: `
  <daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilot.Angular.Scheduler;

  events: any[] = [];

  config: any = {
    timeHeaders : [
      {groupBy: "Month", format: "MMMM yyyy"},
      {groupBy: "Day", format: "d"}
    ],
    eventHeight: 40,
    scale: "Day",
    days: 31,
    startDate: "2017-01-01",
    treeEnabled: true,
    // ...
  };


  constructor(private ds: DataService, private cdr: ChangeDetectorRef) {}

  ngAfterViewInit(): void {
    this.ds.getResources().subscribe(result => this.config.resources = result);

    var from = this.scheduler.control.visibleStart();
    var to = this.scheduler.control.visibleEnd();
    this.ds.getEvents(from, to).subscribe(result => {
      this.events = result;

      // this is required for getEvents() that resolves immediately (no http)
      this.cdr.detectChanges();
    });
  }

}

Row Header Context Menu

angular2-scheduler-resource-context-menu.png

We want to let users access resource-related actions using a context menu. Our context menu will have three items:

  • Edit
  • Delete
  • Add Child

The menu can be created using DayPilot.Menu object:

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      {text: "Edit..." },
      {text: "Delete" },
      {text: "-"},   // separator
      {text: "Add Child..."}
    ]
  });

In later steps, we will add custom event handler to each of the items:

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      {text: "Edit...", onClick: args => this.edit.show(args.source) },
      {text: "Delete", onClick: args => {
        let row = args.source;
        this.ds.deleteResource(row).subscribe(result => {
          row.remove();
          this.scheduler.control.message("Deleted");
        });
      } },
      {text: "-"},
      {text: "Add Child...", onClick: args => this.create.show(args.source) }
    ]
  });

At this moment we just need to assign the context menu to row headers (resources) using "contextMenuResource" property of the config. The context menu can be activated by right-clicking the row header.

  config: any = {
    // ...
    contextMenuResource: this.menu
  };

This is how our SchedulerComponent looks like now:

scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
import {EditComponent} from "./edit.component";
import {CreateComponent} from "./create.component";

@Component({
  selector: 'scheduler-component',
  template: `
  <daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilot.Angular.Scheduler;

  events: any[] = [];

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      {text: "Edit...", onClick: args => this.edit.show(args.source) },
      {text: "Delete", onClick: args => {
        let row = args.source;
        this.ds.deleteResource(row).subscribe(result => {
          row.remove();
          this.scheduler.control.message("Deleted");
        });
      } },
      {text: "-"},
      {text: "Add Child...", onClick: args => this.create.show(args.source) }
    ]
  });


  config: any = {
    timeHeaders : [
      {groupBy: "Month", format: "MMMM yyyy"},
      {groupBy: "Day", format: "d"}
    ],
    eventHeight: 40,
    scale: "Day",
    days: 31,
    startDate: "2017-01-01",
    treeEnabled: true,
    // ...
    contextMenuResource: this.menu
  };


  // ...

}

Context Menu Hint Icon (Active Area)

angular2-scheduler-resource-active-area.png

In addition to right-click activation, we also add an explicit indicator that a context menu is available. We will add an icon in the upper-right corner of the row header that will appear on hover. The icon is created using a customizable object called an active area.

The active area uses one of the predefined icons ("icon-triangle-down") from the DayPilot icon font (see the "icons" directory).

  config: any = {
    // ...
    onBeforeRowHeaderRender: args => {
      args.row.areas = [
        {
          right: 2,
          top: 2,
          icon: "icon-triangle-down",
          style: "font-size: 12px; background-color: #f9f9f9; border: 1px solid #ccc; padding: 2px 2px 0px 2px; cursor:pointer",
          visibility: "Hover",
          action: "ContextMenu"
        }
      ];
    }
  };

scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
import {EditComponent} from "./edit.component";
import {CreateComponent} from "./create.component";

@Component({
  selector: 'scheduler-component',
  template: `
  <daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilot.Angular.Scheduler;

  events: any[] = [];

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      {text: "Edit...", onClick: args => this.edit.show(args.source) },
      {text: "Delete", onClick: args => {
        let row = args.source;
        this.ds.deleteResource(row).subscribe(result => {
          row.remove();
          this.scheduler.control.message("Deleted");
        });
      } },
      {text: "-"},
      {text: "Add Child...", onClick: args => this.create.show(args.source) }
    ]
  });


  config: any = {
    timeHeaders : [
      {groupBy: "Month", format: "MMMM yyyy"},
      {groupBy: "Day", format: "d"}
    ],
    eventHeight: 40,
    scale: "Day",
    days: 31,
    startDate: "2017-01-01",
    treeEnabled: true,
    // ...
    contextMenuResource: this.menu,
    onBeforeRowHeaderRender: args => {
      args.row.areas = [
        {
          right: 2,
          top: 2,
          icon: "icon-triangle-down",
          style: "font-size: 12px; background-color: #f9f9f9; border: 1px solid #ccc; padding: 2px 2px 0px 2px; cursor:pointer",
          visibility: "Hover",
          action: "ContextMenu"
        }
      ];
    }
  };


  // ...

}

Creating Groups (Top-Level Resources)

angular2-scheduler-resource-management-new-group.png

The Scheduler has a built-in helper for creating new resources at the top level. You can activate it using "rowCreateHandling" property of the config:

  config: any = {
    // ...
    rowCreateHandling: "Enabled",
  };

When in-line row creating is enabled, the Scheduler will display an extra row at the bottom. After clicking the row header the user can enter a name of the new row:

angular2-scheduler-resource-management-group-name.png

After a users hits <enter> the Scheduler fire onRowCreate event handler. We will use this event handler to notify the server-side backend.

scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
import {EditComponent} from "./edit.component";
import {CreateComponent} from "./create.component";

@Component({
  selector: 'scheduler-component',
  template: `
  <daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilot.Angular.Scheduler;

  events: any[] = [];

  config: any = {
    // ...

    rowCreateHandling: "Enabled",
    rowCreateHeight: 25,
    rowCreateHtml: "New group...",
    onRowCreate: args => {
      // notify the backend; when successful, add the new resource to the config to display it
      this.ds.createResource(null, args.text).subscribe(result => {
        this.config.resources.push(result);
        this.scheduler.control.message("Created.");
      });
    },

  };

}

Creating Child Resources

angular2-scheduler-resource-management-add-child.png

One of the context menu items lets users add a new child resource.

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      // ...
      {text: "Add Child...", onClick: args => this.create.show(args.source) }
    ]
  });

scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
import {EditComponent} from "./edit.component";
import {CreateComponent} from "./create.component";

@Component({
  selector: 'scheduler-component',
  template: `
  <daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>
  ...
  <create-dialog #create (close)="createClosed($event)"></create-dialog>
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilot.Angular.Scheduler;
  @ViewChild("create") create: CreateComponent;

  events: any[] = [];

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      {text: "Edit...", onClick: args => this.edit.show(args.source) },
      {text: "Delete", onClick: args => {
        let row = args.source;
        this.ds.deleteResource(row).subscribe(result => {
          row.remove();
          this.scheduler.control.message("Deleted");
        });
      } },
      {text: "-"},
      {text: "Add Child...", onClick: args => this.create.show(args.source) }
    ]
  });

  config: any = {
    //...
    contextMenuResource: this.menu,
    onBeforeRowHeaderRender: args => {
      args.row.areas = [
        {
          right: 2,
          top: 2,
          icon: "icon-triangle-down",
          style: "font-size: 12px; background-color: #f9f9f9; border: 1px solid #ccc; padding: 2px 2px 0px 2px; cursor:pointer",
          visibility: "Hover",
          action: "ContextMenu"
        }
      ];
    }

  };

  constructor(private ds: DataService, private cdr: ChangeDetectorRef) {}

  createClosed(args) {
    if (args.result) {
      this.scheduler.control.message("Created");
    }
  }

}

The "create" object holds a reference to a modal dialog with new resource details. We have created the modal dialog using DayPilot.Angular.Modal helper. See also a special tutorial that explains using Angular 2 modal dialogs with DayPilot Scheduler.

angular2-scheduler-resource-management-create-dialog.png

The onClick event handler of the "Add Child...." menu items opens the modal dialog using .show() method and passes the menu source (args.source) to it. The args.source objects holds a DayPilot.Row object with row details.

create.component.ts

import {Component, ViewChild, Output, EventEmitter} from '@angular/core';
import {DayPilot} from "daypilot-pro-angular";
import Modal = DayPilot.Angular.Modal;
import {Validators, FormBuilder, FormGroup, FormControl} from "@angular/forms";
import {DataService, CreateEventParams, EventData, UpdateEventParams} from "./data.service";

@Component({
  selector: 'create-dialog',
  template: `
    <daypilot-modal #modal (close)="closed($event)">
    <div class="center">
      <h1>Create Resource</h1>
      <form [formGroup]="form">
        <div class="form-item">
          <input formControlName="name" type="text" placeholder="Resource Name"> <span *ngIf="!form.controls.name.valid">Resource name required</span>
        </div>
        <div class="form-item">
          <button (click)="submit()" [disabled]="!form.valid">Save</button>
          <button (click)="cancel()">Cancel</button>
        </div>
    </form>
    </div>
    </daypilot-modal>
  `,
  styles: [`
  .center {
    max-width: 800px;
    margin-left: auto;
    margin-right: auto;
  }
  .form-item {
    margin: 4px 0px;
  }
  `]
})
export class CreateComponent {
  @ViewChild("modal") modal : Modal;
  @Output() close = new EventEmitter();

  form: FormGroup;

  row: DayPilot.Row;

  constructor(private fb: FormBuilder, private ds: DataService) {
    this.form = this.fb.group({
      name: ["", Validators.required],
    });
  }

  show(row: DayPilot.Row) {
    this.row = row;
    this.form.setValue({
      name: "",
    });
    this.modal.show();
  }

  submit() {
    let data = this.form.getRawValue();

    this.ds.createResource(this.row, data.name).subscribe(resource => {

      // add the new resource to the tree
      let parent = this.row.data;
      parent.expanded = true;
      if (!parent.children) {
        parent.children = [];
      }
      parent.children.push(resource);

      this.modal.hide(resource);
    });
  }

  cancel() {
    this.modal.hide();
  }

  closed(args) {
    this.close.emit(args);
  }

}

Deleting Resources

angular2-scheduler-resource-management-delete.png

In our example, we use a context menu item to delete a resource:

  menu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      // ...

      {text: "Delete", onClick: args => {
        let row = args.source;
        this.ds.deleteResource(row).subscribe(result => {
          row.remove();
          this.scheduler.control.message("Deleted");
        });
      } },

      // ...
    ]
  });

The onClick event handler directly calls the DataService class. On success, it removes the row from the Scheduler using DayPilot.Row.remove() method.

Moving Resources using Drag and Drop

angular2-scheduler-resource-management-move-handle.png

The Scheduler supports drag and drop row moving. This feature is disabled by default - you need to enable it using "rowMoveHandling" property of the config:

  config: any = {

    // ...
  
    rowMoveHandling: "Update",

    // ...

  };

When you enable drag and drop row moving the row will display a move handle on hover. Users can drag the row by the handle and move it to a new location. The source row remains highlighted during dragging.

angular2-scheduler-resource-management-moving.png

As soon as a user drops the row at the target location "onRowMoved" event handler is fired. The event args hold the operation details:

  • args.source - DayPilot.Row object with source row
  • args.target - the target DayPilot.Row object
  • args.position - drop position relative to target ("child" |  "before" | "after")
import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
import {EditComponent} from "./edit.component";
import {CreateComponent} from "./create.component";

@Component({
  selector: 'scheduler-component',
  template: `
  <daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilot.Angular.Scheduler;

  events: any[] = [];

  config: any = {

    // ...
  
    rowMoveHandling: "Update",
    onRowMoved: args => {
      this.ds.moveResource(args.source, args.target, args.position).subscribe(result => this.scheduler.control.message("Moved"));
    },
    
    // ...

  };


}