Features

  • Angular timetable component created using Angular Calendar component (week view) from DayPilot Pro for JavaScript
  • Custom time blocks displayed on the vertical axis (instead of hours of day)
  • Blocks are mapped to hour numbers
  • Includes a trial version of DayPilot Pro for JavaScript (see License below) 

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.

Angular Timetable Based on Weekly Calendar

angular-timetable-calendar-week-view.png

We will start with a new Angular 7 project generated using Calendar UI Builder.

The initial configuration is every simple: It displays the calendar switched to a week view. It shows the first 7 hours of a day, one cell per hour.

config: any = {
  viewType: "Week",
  cellDuration: 60,
  dayBeginsHour: 0,
  dayEndsHour: 7,
  businessBeginsHour: 0,
  businessEndsHour: 7,
  // ...
};

We also add a testing event:

events: any[] = [
  {
    id: 1,
    text: "Event 1",
    start: "2018-11-05T01:00:00",
    end: "2018-11-05T03:00:00"
  }
];

This is how the first version of our Angular application looks like:

src/app/timetable/timetable.component.ts

import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotCalendarComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";{}

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

  @ViewChild("timetable") timetable: DayPilotCalendarComponent;

  events: any[] = [
    {
      id: 1,
      text: "Event 1",
      start: "2018-11-05T01:00:00",
      end: "2018-11-05T03:00:00"
    }
  ];

  config: any = {
    viewType: "Week",
    dayBeginsHour: 0,
    dayEndsHour: 7,
    businessBeginsHour: 0,
    businessEndsHour: 7,
    headerHeight: 30,
    hourWidth: 100,
    cellDuration: 60,
    cellHeight: 60,
    durationBarVisible: false,
    headerDateFormat: "dddd M/d/yyyy"
  };
  
  constructor(private ds: DataService) {
  }

  ngAfterViewInit(): void {
  }
  
}

Timetable Blocks

angular-timetable-time-blocks.png

In the next step, we will override the time header (on the left side) to display custom time blocks instead of hours.

We define the time blocks using an array:

blocks: any[] = [
  {name: "Block 1"},
  {name: "Block 2"},
  {name: "Block 3"},
  {name: "Block 4"},
  {name: "Block 5"},
  {name: "Block 6"},
  {name: "Block 7"}
];

And modify the calendar component config object to display the specified number of slots (hours):

config: any = {
  dayBeginsHour: 0,
  dayEndsHour: this.blocks.length,
  businessBeginsHour: 0,
  businessEndsHour: this.blocks.length,
  // ...
};

We will use onBeforeTimeHeader render event handler to override the default row header text:

config: any = {
  // ...
  onBeforeTimeHeaderRender: args => {
    let hour = args.header.time.hours();
    let block = this.blocks[hour];
    if (block) {
      args.header.html = block.name;
    }
  }
};

src/app/timetable/timetable.component.ts

import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotCalendarComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";{}

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

  @ViewChild("timetable") timetable: DayPilotCalendarComponent;

  events: any[] = [
    {
      id: 1,
      text: "Event 1",
      start: "2018-11-05T01:00:00",
      end: "2018-11-05T03:00:00"
    }
  ];

  blocks: any[] = [
    {name: "Block 1"},
    {name: "Block 2"},
    {name: "Block 3"},
    {name: "Block 4"},
    {name: "Block 5"},
    {name: "Block 6"},
    {name: "Block 7"}
  ];

  config: any = {
    viewType: "Week",
    dayBeginsHour: 0,
    dayEndsHour: this.blocks.length,
    businessBeginsHour: 0,
    businessEndsHour: this.blocks.length,
    headerHeight: 30,
    hourWidth: 100,
    cellDuration: 60,
    cellHeight: 60,
    durationBarVisible: false,
    headerDateFormat: "dddd M/d/yyyy",
    onBeforeTimeHeaderRender: args => {
      let hour = args.header.time.hours();
      let block = this.blocks[hour];
      if (block) {
        args.header.html = block.name;
      }
    }
  };

  constructor(private ds: DataService) {
  }

  ngAfterViewInit(): void {
  }

}

Mapping Timetable Data

angular-timetable-data-mapping.png

Note that the previous example still defined the event data using the original format (start and end properties specifying the date and time of the underlying time slot):

events: any[] = [
  {
    id: 1,
    text: "Event 1",
    start: "2018-11-05T01:00:00",
    end: "2018-11-05T03:00:00"
  }
];

You may choose to use this format to store the data in the database. It is convenient because it only requires two date/time fields but reading the raw data may not be intuitive.

In this example, we will use a custom data format. Instead of start and end fields with date/time values, we will use date, block and duration

events: any[] = [
  {
    id: 1,
    date: "2018-11-05",
    block: 1,
    duration: 2,
    text: "Event 1"
  }
];

However, this underlying Angular calendar component still works with the full date/time format so we need to add methods that will convert the event data between the two formats:

blockDataToEvent(e: any): any {
  let date = new DayPilot.Date(e.date);
  return {
    id: e.id,
    start: date.addHours(e.block),
    end: date.addHours(e.block).addHours(e.duration),
    text: e.text,
    additional: e.additional
  };
}

eventToBlockData(data: any): any {
  let date = data.start.getDatePart();
  return {
    id: data.id,
    text: data.text,
    date: date,
    block: data.start.getHours(),
    duration: data.end.getHours() - data.start.getHours(),
    additional: data.additional
  };
}

We will use the blockDataToEvent() method to map the data loaded from an external source:

ngAfterViewInit(): void {
  var from = this.timetable.control.visibleStart();
  var to = this.timetable.control.visibleEnd();
  this.ds.getEvents(from, to).subscribe(result => {
    this.events = result.map(data => this.blockDataToEvent(data));
  });
}

src/app/timetable/timetable.component.ts

import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotCalendarComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";{}

@Component({
  selector: 'timetable-component',
  template: `<daypilot-calendar [config]="config" [events]="events" #timetable></daypilot-calendar>
  <div class="space">
    <h2>Raw data</h2>
    <div *ngFor="let item of rawData">{{item}}</div>
  </div>
  `,
  styles: [` /* ... */ `]
})
export class TimetableComponent implements AfterViewInit {

  @ViewChild("timetable") timetable: DayPilotCalendarComponent;

  events: any[] = [];

  blocks: any[] = [
    {name: "Block 1"},
    {name: "Block 2"},
    {name: "Block 3"},
    {name: "Block 4"},
    {name: "Block 5"},
    {name: "Block 6"},
    {name: "Block 7"}
  ];

  config: any = {
    viewType: "Week",
    dayBeginsHour: 0,
    dayEndsHour: this.blocks.length,
    businessBeginsHour: 0,
    businessEndsHour: this.blocks.length,
    headerHeight: 30,
    hourWidth: 100,
    cellDuration: 60,
    cellHeight: 60,
    durationBarVisible: false,
    headerDateFormat: "dddd M/d/yyyy",
    onBeforeTimeHeaderRender: args => {
      let hour = args.header.time.hours();
      let block = this.blocks[hour];
      if (block) {
        args.header.html = block.name;
      }
    }
  };

  get rawData(): string[] {
    return this.events.map(e => JSON.stringify(this.eventToBlockData(e)));
  }
  
  constructor(private ds: DataService) {
  }

  ngAfterViewInit(): void {
    var from = this.timetable.control.visibleStart();
    var to = this.timetable.control.visibleEnd();
    this.ds.getEvents(from, to).subscribe(result => {
      this.events = result.map(data => this.blockDataToEvent(data));
    });
  }


  blockDataToEvent(e: any): any {
    let date = new DayPilot.Date(e.date);
    return {
      id: e.id,
      start: date.addHours(e.block),
      end: date.addHours(e.block).addHours(e.duration),
      text: e.text,
      additional: e.additional
    };
  }

  eventToBlockData(data: any): any {
    let date = data.start.getDatePart();
    return {
      id: data.id,
      text: data.text,
      date: date,
      block: data.start.getHours(),
      duration: data.end.getHours() - data.start.getHours(),
      additional: data.additional
    };
  }

}

src/app/timetable/data.service.ts

import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {DayPilot} from "daypilot-pro-angular";
import {HttpClient} from "@angular/common/http";

@Injectable()
export class DataService {

  events: any[] = [
    {
      id: 1,
      date: DayPilot.Date.today(),
      block: 1,
      duration: 2,
      text: "Event 1"
    },
    {
      id: 2,
      date: DayPilot.Date.today(),
      block: 3,
      duration: 1,
      text: "Event 2"
    },
    {
      id: 3,
      date: DayPilot.Date.today(),
      block: 4,
      duration: 1,
      text: "Event 3"
    }
  ];

  constructor(private http : HttpClient){
  }

  getEvents(from: DayPilot.Date, to: DayPilot.Date): Observable<any[]> {

    // simulating an HTTP request
    return new Observable(observer => {
      setTimeout(() => {
        observer.next(this.events);
        observer.complete();
      }, 200);
    });

  }

  addEvent(data: any) {
    this.events.push(data);
  }

}

Timetable Event Colors

angular-timetable-event-colors.png

In the final step, we will extend our Angular timetable component to display the events using a specified color.

We will specify the color using "additional" object of the data item:

events: any[] = [
  {
    id: 1,
    date: DayPilot.Date.today(),
    block: 1,
    duration: 2,
    text: "Event 1",
    additional: {
      color: "#f9cb9c"
    }
  },
  {
    id: 2,
    date: DayPilot.Date.today(),
    block: 3,
    duration: 1,
    text: "Event 2",
    additional: {
      color: "#ffe599"
    }
  },
  {
    id: 3,
    date: DayPilot.Date.today(),
    block: 4,
    duration: 1,
    text: "Event 3",
    additional: {
      color: "#b6d7a8"
    }
  }
];

We will apply the color using onBeforeEventRender event handler that lets us customize the event rendering:

config: any = {
  // ...
  onBeforeEventRender: args => {
    if (args.data.additional && args.data.additional.color) {
      args.data.backColor = args.data.additional.color;
      args.data.borderColor = this.darken(args.data.backColor, 0.1);
    }
  }
}

Full Source Code

src/app/timetable/timetable.component.ts

import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotCalendarComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";{}

@Component({
  selector: 'timetable-component',
  template: `<daypilot-calendar [config]="config" [events]="events" #timetable></daypilot-calendar>
  <div class="space">
    <h2>Raw data</h2>
    <div *ngFor="let item of rawData">{{item}}</div>
  </div>
  `,
  styles: [`
    :host ::ng-deep .calendar_default_rowheader_inner {
      text-align: left;
      padding: 5px;
      font-size: 14px;
      display: flex;
      align-items: center;
    }

    :host ::ng-deep .calendar_default_colheader_inner {
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 14px;
    }

    :host ::ng-deep .calendar_default_event_inner {
      color: #333;
    }
  `]
})
export class TimetableComponent implements AfterViewInit {

  @ViewChild("timetable") timetable: DayPilotCalendarComponent;

  events: any[] = [];

  blocks: any[] = [
    {name: "Block 1"},
    {name: "Block 2"},
    {name: "Block 3"},
    {name: "Block 4"},
    {name: "Block 5"},
    {name: "Block 6"},
    {name: "Block 7"}
  ];

  config: any = {
    viewType: "Week",
    dayBeginsHour: 0,
    dayEndsHour: this.blocks.length,
    businessBeginsHour: 0,
    businessEndsHour: this.blocks.length,
    headerHeight: 30,
    hourWidth: 100,
    cellDuration: 60,
    cellHeight: 60,
    durationBarVisible: false,
    headerDateFormat: "dddd M/d/yyyy",
    onBeforeTimeHeaderRender: args => {
      let hour = args.header.time.hours();
      let block = this.blocks[hour];
      if (block) {
        args.header.html = block.name;
      }
    },
    onBeforeEventRender: args => {
      if (args.data.additional && args.data.additional.color) {
        args.data.backColor = args.data.additional.color;
        args.data.borderColor = this.darken(args.data.backColor, 0.1);
      }
    },
    onTimeRangeSelected: args => {
      let dp = this.timetable.control;
      let component = this;
      DayPilot.Modal.prompt("Create a new event:", "Event 1").then(function(modal) {

        dp.clearSelection();
        if (!modal.result) { return; }

        let eventData = {
          start: args.start,
          end: args.end,
          id: DayPilot.guid(),
          text: modal.result,
          additional: {
            color: "#a4c2f4"
          }
        };

        dp.events.add(new DayPilot.Event(eventData));

        let blockData = component.eventToBlockData(eventData);
        console.log(blockData);
      });
    },
  };

  get rawData(): string[] {
    return this.events.map(e => JSON.stringify(this.eventToBlockData(e)));
  }

  darken(color: string, k: number): string {
    if (color[0] !== "#" || color.length !== 7) {
      throw "Color expected in full hex format, e.g. '#ffffff'";
    }
    let R = parseInt(color.substring(1, 3),16);
    let G = parseInt(color.substring(3, 5),16);
    let B = parseInt(color.substring(5, 7),16);
    return "#" + factor(R, k) + factor(G, k) + factor(B, k);

    function factor(c: number, k: number): string {
      let v = c*(1 - k);
      v = Math.round(v);
      v = Math.min(v, 255);
      v = Math.max(v, 0);
      return v.toString(16);
    }
  }

  constructor(private ds: DataService) {
  }

  ngAfterViewInit(): void {
    var from = this.timetable.control.visibleStart();
    var to = this.timetable.control.visibleEnd();
    this.ds.getEvents(from, to).subscribe(result => {
      this.events = result.map(data => this.blockDataToEvent(data));
    });
  }


  blockDataToEvent(e: any): any {
    let date = new DayPilot.Date(e.date);
    return {
      id: e.id,
      start: date.addHours(e.block),
      end: date.addHours(e.block).addHours(e.duration),
      text: e.text,
      additional: e.additional
    };
  }

  eventToBlockData(data: any): any {
    let date = data.start.getDatePart();
    return {
      id: data.id,
      text: data.text,
      date: date,
      block: data.start.getHours(),
      duration: data.end.getHours() - data.start.getHours(),
      additional: data.additional
    };
  }

}