Overview

  • How to detect the current viewport orientation (portrait or landscape).

  • Watch for device rotation and update the Angular Calendar view to display the appropriate view.

  • This project defines three views: full-width landscape mode for desktop devices, mini landscape mode that displays three days and a horizontal scrollbar and a portrait mode that displays a daily calendar.

  • For an introduction to using the Angular calendar component, please see the Angular Appointment Calendar Component (TypeScript + PHP/MySQL) 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.

How to get the current device orientation

To get the current orientation, you can use the following media query string and check if the document matches using window.matchMedia() method. The query compares viewport width and height; it does not read a physical device sensor. Read the returned MediaQueryList.matches property to get a boolean:

const isLandscape = window.matchMedia("(orientation: landscape)").matches;

How to detect device rotation

A media query can report orientation changes using a change event listener. This sample listens to the window resize event instead: rotation changes the viewport size, and resizing without rotating must also recalculate the calendar column widths.

The resize listener calls viewUpdate(), which checks both media queries. We also call it once in ngAfterViewInit() to select the initial view. Updates that would leave the view and column width unchanged are skipped. The stored callback lets ngOnDestroy() remove the listener. If you use a media-query listener in older code, note that addListener() is the legacy API.

export class CalendarComponent implements AfterViewInit, OnDestroy {

  // ...
  private readonly onResize = () => this.viewUpdate();

  // ...

  ngOnDestroy(): void {
    window.removeEventListener("resize", this.onResize);
  }

  ngAfterViewInit(): void {
    this.viewUpdate();
    window.addEventListener("resize", this.onResize);
    // ...
  }

}

How to switch the Angular Calendar view on device rotation

We will define three Angular calendar views. The standalone component stores its configuration in an Angular signal. Each view method uses config.update() to return a new configuration object so that the DayPilot wrapper receives the change in a zoneless application.

1. Full-width landscape view. This view will be used for desktop devices and tablets.

viewFull(): void {
  if (this.config().viewType === "Week" && this.config().columnWidthSpec === "Auto") { return; }
  this.config.update(config => ({...config, columnWidthSpec: "Auto", viewType: "Week"}));
}

2. Small landscape view. This view will display one week but it uses a fixed column width so that approximately three days fit in the viewport. The remaining days are accessible using the horizontal scrollbar:

viewLandscape(): void {
  const w = this.host.nativeElement.clientWidth - (this.calendar.control.hourWidth || 0) - 2;
  const columnWidth = Math.floor(w / 3);
  if (this.config().columnWidthSpec === "Fixed" && this.config().columnWidth === columnWidth) { return; }
  this.config.update(config => ({
    ...config,
    columnWidthSpec: "Fixed",
    columnWidth,
    viewType: "Week"
  }));
}

We measure the component host instead of using the full window width, subtract the hour header and borders, and divide the remaining width by three. The host uses display: block so that clientWidth represents the available content width.

angular calendar small landscape

3. Portrait view. The portrait mode will display one day.

viewPortrait(): void {
  if (this.config().viewType === "Day") { return; }
  this.config.update(config => ({...config, columnWidthSpec: "Auto", viewType: "Day"}));
}

angular calendar portrait

When deciding which view to use, we will also need to check the device width. We will use the small landscape view for devices with up to 800 pixels:

smallWidth = window.matchMedia("(max-width: 800px)");

The view-switching logic now looks like this:

viewUpdate(): void {
  const isLandscape = window.matchMedia("(orientation: landscape)").matches;
  const isSmall = this.smallWidth.matches;

  if (isLandscape) {
    if (isSmall) {
      this.viewLandscape();
    }
    else {
      this.viewFull();
    }
  }
  else {
    this.viewPortrait();
  }
}

Here is the source code of the Angular calendar with the complete device rotation detection logic. The sample service loads a current-day event; selecting a time range opens the event-creation dialog:

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

@Component({
  selector: 'calendar-component',
  standalone: true,
  imports: [DayPilotModule],
  providers: [DataService],
  template: `<daypilot-calendar [config]="config" [events]="events" #calendar></daypilot-calendar>`,
  styles: [`:host { display: block; }`]
})
export class CalendarComponent implements AfterViewInit, OnDestroy {

  @ViewChild("calendar")
  calendar!: DayPilotCalendarComponent;

  smallWidth = window.matchMedia("(max-width: 800px)");
  private readonly onResize = () => this.viewUpdate();

  events = signal<DayPilot.EventData[]>([]);

  config = signal<DayPilot.CalendarConfig>({
    cellHeight: 30,
    durationBarVisible: false,
    eventClickHandling: "Disabled",
    eventHoverHandling: "Disabled",
    eventMoveHandling: "Update",
    eventResizeHandling: "Update",
    onEventMoved: ((args) => {
      console.log("Event moved: " + args.e.text());
    }),
    onEventResized: ((args) => {
      console.log("Event resized: " + args.e.text());
    }),
    onTimeRangeSelected: (async (args) => {
      const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
      const calendar = args.control;
      calendar.clearSelection();
      if (modal.canceled) {
          return;
      }
      calendar.events.add({
          start: args.start,
          end: args.end,
          id: DayPilot.guid(),
          text: modal.result
      });
    }),
    timeRangeSelectedHandling: "Enabled",
    columnWidthSpec: "Auto",
    viewType: "Week",
  });

  constructor(private ds: DataService, private host: ElementRef<HTMLElement>) {
  }

  viewUpdate(): void {
    const isLandscape = window.matchMedia("(orientation: landscape)").matches;
    const isSmall = this.smallWidth.matches;

    if (isLandscape) {
      if (isSmall) {
        this.viewLandscape();
      }
      else {
        this.viewFull();
      }
    }
    else {
      this.viewPortrait();
    }
  }

  viewLandscape(): void {
    const w = this.host.nativeElement.clientWidth - (this.calendar.control.hourWidth || 0) - 2;
    const columnWidth = Math.floor(w / 3);
    if (this.config().columnWidthSpec === "Fixed" && this.config().columnWidth === columnWidth) { return; }
    this.config.update(config => ({
      ...config,
      columnWidthSpec: "Fixed",
      columnWidth,
      viewType: "Week"
    }));
  }

  viewPortrait(): void {
    if (this.config().viewType === "Day") { return; }
    this.config.update(config => ({...config, columnWidthSpec: "Auto", viewType: "Day"}));
  }

  viewFull(): void {
    if (this.config().viewType === "Week" && this.config().columnWidthSpec === "Auto") { return; }
    this.config.update(config => ({...config, columnWidthSpec: "Auto", viewType: "Week"}));
  }

  ngOnDestroy(): void {
    window.removeEventListener("resize", this.onResize);
  }

  ngAfterViewInit(): void {
    this.viewUpdate();
    window.addEventListener("resize", this.onResize);
    const from = this.calendar.control.visibleStart();
    const to = this.calendar.control.visibleEnd();
    this.ds.getEvents(from, to).subscribe(result => {
      this.events.set(result);
    });
  }

}