---
title: "Angular Event calendar Google Calendar Sync Example | Mobiscroll"
component: "Event calendar"
framework: "Angular"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/angular/eventcalendar/sync-events-google-calendar"
---

# Google Calendar Sync - Angular Event calendar demo

Connect Google Calendar to a Angular event calendar — authenticate users, load events, and sync create, edit, and delete actions to Google Calendar.

- [Live demo](https://demo.mobiscroll.com/angular/eventcalendar/sync-events-google-calendar)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-angular/tree/main/src/demos/eventcalendar/calendar-view/sync-events-google-calendar)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/eventcalendar/sync-events-google-calendar.md), [jQuery](https://demo.mobiscroll.com/jquery/eventcalendar/sync-events-google-calendar.md), [React](https://demo.mobiscroll.com/react/eventcalendar/sync-events-google-calendar.md), [Vue](https://demo.mobiscroll.com/vue/eventcalendar/sync-events-google-calendar.md)

## Description

The event calendar comes with third party calendar integration support through the [**integration plugin**](https://mobiscroll.com/docs/angular/eventcalendar/calendar-integrations). This needs to be added to the download package or installed separately from a dedicated NPM package.

It includes everything you need to authenticate your users, get their Google calendars and provides functions for CRUD operations. It takes care of loading the events and converts them to the required format so that they can be displayed on the Mobiscroll calendar separately or mixed with other calendars and events.

Customizing the interaction, event workflows, whether the events are read-only is up to you. In this live demo, newly created events will land in the `primary calendar` of the authenticated user.

This example uses the [**integration plugin**](https://mobiscroll.com/docs/angular/eventcalendar/calendar-integrations) to connect to and load events from Google Calendar and it is not available in the trial. Give the live demo a try and see how it can be configured.

## What this demo shows

- Shows how Mobiscroll event calendar can load and display events from a signed-in Google Calendar account in a desktop month view.
- **Layout** The demo uses a full month calendar layout with a separate panel on the left for Google authentication, editing settings, and calendar selection.
- **Signed-out state** No events are loaded by default, and the side panel prompts the user to sign in with the text "Log into your Google account to view and edit your Google Calendar events" followed by a `Sign in with Google` button.
- **Authentication flow** Clicking the Google sign-in button opens a new browser tab where the user can authenticate with their Google account.
- **Signed-in state** After sign-in, the side panel changes to an authenticated state with guidance about editing events and syncing changes back to Google Calendar.
- **Editing toggle** The authenticated panel includes an `Enable editing` switch, which is off by default and controls whether event edits are allowed or not.
- **Editing toggle enabled** Editing an event opens a popup with the following message `Are you sure you want to update this event? This actin will affect your Google Calendar event.` and below that ther is a `Cancel` and `Update` button, by clicking the firs one, the popup closes and the event editing won't happen and by clicking the second button will update the event on Mobiscroll calendar and sync back to the Google Calendar as well. 
- **Event creation or update** If a new event is created or an existing one updated, a `Event created/updated/ deleted in [the name of your Google Calendar] calendar` toast appears at the bottom center of the calendar.
- **Calendar list** The `My Calendars` section shows multiple calendar switches, such as the user's personal calendar and country-specific holiday calendars.
- **Sign-out** The bottom of the side panel includes a `Log out of my account` button for disconnecting the Google Calendar account.
- **Event rendering** Once events are loaded, days with events display event labels inside the month cells.
- **Event styling** Event labels can appear with different visual styles based on the type or source of the event they represent.
- **Event interaction** Hovering over or selecting an event label highlights it. If event editing is enabled this action shows resize handles on both sides, indicating that the event can be resized by clicking and dragging.
- **Day cell states** Hovering a day cell highlights the day number with a gray background, while clicking the empty part of the cell selects the day and highlights the day number with a blue background.
- **Header navigation** The calendar header shows the current month and year on the left, and blue month navigation arrows with a `Today` button between them on the right.
- **Swipe navigation** When event editing is disabled, the displayed month can also be changed by dragging the calendar left or right.

## Best for

- **Personal scheduling interfaces** Building calendar views where users need to browse and manage their own events from an existing Google Calendar.
- **Calendar overlay experiences** Letting users toggle multiple Google calendars, such as personal and holiday calendars, within the same event calendar.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`labels`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-labels) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onPageLoading`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreate) - event
- [`onEventUpdate`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdate) - event
- [`onEventDelete`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDelete) - event
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`exclusiveEndDates`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-exclusiveEndDates) - option
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`headerTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-headerTemplate) - template

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { Component, NgZone, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MbscCalendarEvent, MbscEventcalendarOptions, MbscModule, Notifications, setOptions } from '@mobiscroll/angular';
import { googleCalendarSync } from '@mobiscroll/calendar-integration';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  providers: [Notifications],
  standalone: true,
  imports: [CommonModule, FormsModule, MbscModule],
})
export class AppComponent implements OnInit {
  constructor(
    public notify: Notifications,
    public zone: NgZone,
  ) {}

  myEvents: MbscCalendarEvent[] = [];

  isEditable = false;
  isLoggedIn = false;
  isLoading = false;
  isHidden = true;
  calendarIds: string[] = [];
  myCalendars: any = [];
  startDate!: Date;
  endDate!: Date;
  debounce: any;
  calendarData: any = {};
  primaryCalendarId!: string;

  calendarOptions: MbscEventcalendarOptions = {
    view: {
      calendar: { labels: true },
    },
    extendDefaultEvent: () => ({
      color: this.calendarData[this.primaryCalendarId].color,
    }),
    onPageLoading: (args: any) => {
      this.startDate = args.viewStart;
      this.endDate = args.viewEnd;
      clearTimeout(this.debounce);
      this.debounce = setTimeout(() => {
        if (googleCalendarSync.isSignedIn()) {
          this.isLoading = true;
          googleCalendarSync
            .getEvents(this.calendarIds, this.startDate, this.endDate)
            .then((resp: any) => {
              this.zone.run(() => {
                this.myEvents = resp;
                this.isLoading = false;
              });
            })
            .catch((error: any) => {
              this.onError(error);
            });
        }
      }, 200);
    },
    onEventCreate: (args: any) => {
      if (googleCalendarSync.isSignedIn()) {
        const event = args.event;
        googleCalendarSync
          .addEvent(this.primaryCalendarId, event)
          .then((resp: any) => {
            this.zone.run(() => {
              resp.color = event.color;
              this.myEvents = [...this.myEvents.filter((item) => item.id !== event.id), resp];
            });
            this.notify.toast({
              message: 'Event created in "' + this.calendarData[this.primaryCalendarId].name + '" calendar',
            });
          })
          .catch((error: any) => {
            this.zone.run(() => {
              this.myEvents = this.myEvents.filter((item) => item.id !== event.id);
              this.onError(error);
            });
          });
      }
    },
    onEventUpdate: (args) => {
      if (googleCalendarSync.isSignedIn()) {
        this.notify
          .confirm({
            title: 'Are you sure you want to update this event?',
            message: 'This action will affect your Google Calendar event.',
            okText: 'Update',
          })
          .then((result) => {
            const event = args.event;
            if (result) {
              const calendarId = event['googleCalendarId'];
              googleCalendarSync
                .updateEvent(calendarId, event)
                .then(() => {
                  this.notify.toast({
                    message: 'Event updated on "' + this.calendarData[calendarId].name + '" calendar',
                  });
                })
                .catch(() => {
                  this.zone.run(() => {
                    this.myEvents = [...this.myEvents.filter((item) => item.id !== event.id), args.oldEvent!];
                  });
                });
            } else {
              this.myEvents = [...this.myEvents.filter((item) => item.id !== event.id), args.oldEvent!];
            }
          });
      }
    },
    onEventDelete: (args) => {
      if (googleCalendarSync.isSignedIn()) {
        this.notify
          .confirm({
            title: 'Are you sure you want to delete this event?',
            message: 'This action will remove the event from your Google Calendar as well.',
            okText: 'Delete',
          })
          .then((result) => {
            if (result) {
              const event = args.event;
              const calendarId = event['googleCalendarId'];
              googleCalendarSync
                .deleteEvent(calendarId, event)
                .then(() => {
                  this.zone.run(() => {
                    this.myEvents = this.myEvents.filter((item) => item.id !== event.id);
                  });
                  this.notify.toast({
                    message: 'Event deleted from "' + this.calendarData[calendarId].name + '" calendar',
                  });
                })
                .catch((error: any) => {
                  this.onError(error);
                });
            }
          });
      }
      return false;
    },
  };

  ngOnInit(): void {
    this.isHidden = false;
    googleCalendarSync.init({
      apiKey: '<YOUR_GOOGLE_API_KEY>',
      clientId: '<YOUR_GOOGLE_CLIENT_ID>',
      onSignedIn: () => {
        this.isLoggedIn = true;
        googleCalendarSync
          .getCalendars()
          .then((calendars: any) => {
            calendars.sort((a: { primary: boolean }) => (a.primary ? -1 : 1));
            this.myCalendars = calendars;
            this.primaryCalendarId = calendars[0].id;
            this.calendarIds = [...this.calendarIds, this.primaryCalendarId];

            for (const c of calendars) {
              this.calendarData[c.id] = { name: c.summary, color: c.backgroundColor, checked: false };
            }

            this.isLoading = true;

            googleCalendarSync
              .getEvents([this.primaryCalendarId], this.startDate, this.endDate)
              .then((resp: any) => {
                this.zone.run(() => {
                  this.myEvents = resp;
                  this.calendarData[this.primaryCalendarId].checked = true;
                  this.isLoading = false;
                });
              })
              .catch((error: any) => {
                this.onError(error);
              });
          })
          .catch((error: any) => {
            this.onError(error);
          });
      },
      onSignedOut: () => {
        this.isLoggedIn = false;
        this.myCalendars = [];
        this.calendarIds = [];
        this.calendarData = {};
        this.myEvents = [];
      },
    });
  }

  onError(resp: any): void {
    this.notify.toast({
      message: resp.error ? resp.error : resp.result.error.message,
    });
  }

  changeEditable(ev: any): void {
    this.isEditable = ev.target.checked;
  }

  toggleCalendars(ev: any, calendarId: string): void {
    const checked = ev.target.checked;
    this.calendarData[calendarId].checked = checked;

    if (checked) {
      this.calendarIds = [...this.calendarIds, calendarId];
    } else {
      this.calendarIds = this.calendarIds.filter((id) => id !== calendarId);
    }

    if (this.calendarIds.length === 0) {
      this.myEvents = [];
      return;
    }

    this.isLoading = true;
    googleCalendarSync
      .getEvents(this.calendarIds, this.startDate, this.endDate)
      .then((resp: any) => {
        this.zone.run(() => {
          this.myEvents = resp;
          this.isLoading = false;
        });
      })
      .catch((error: any) => {
        this.onError(error);
      });
  }

  logOut(): void {
    googleCalendarSync.signOut().catch((error: any) => {
      this.onError(error);
    });
  }

  logIn(): void {
    if (!googleCalendarSync.isSignedIn()) {
      googleCalendarSync.signIn().catch((error: any) => {
        this.onError(error);
      });
    }
  }
}
```

### Template

```html
<mbsc-page class="md-sync-events-google-cont" [ngClass]="{'md-loading-events': isLoading}">
  <div [ngClass]="{'md-sync-events-google-menu': true, 'mbsc-hidden': isHidden}">
    <ng-container *ngIf="!isLoggedIn">
      <div class="mbsc-form-group-inset mbsc-align-center" [attr.aria-hidden]="isLoggedIn">
        <p class="mbsc-italic mbsc-txt-muted">Log into your Google account to view and edit your Google Calendar events</p>
        <button (click)="logIn()" class="mbsc-reset md-sync-events-google-button">Sign in with Google</button>
      </div>
    </ng-container>
    <ng-container *ngIf="isLoggedIn">
      <div [attr.aria-hidden]="!isLoggedIn">
        <div class="mbsc-form-group-inset mbsc-align-center">
          <p class="mbsc-italic mbsc-txt-muted">
            Editing events sync back to your calendar when enabled. You'll be asked for confirmation on every action.
          </p>
        </div>
        <div class="mbsc-form-group-inset">
          <mbsc-switch label="Enable editing" [(ngModel)]="isEditable" (change)="changeEditable($event)"></mbsc-switch>
        </div>
        <div class="mbsc-form-group-inset md-sync-events-google-inset">
          <ng-container *ngFor="let cal of myCalendars;">
            <mbsc-switch
              label="{{cal.summary}}"
              [(ngModel)]="calendarData[cal.id].checked"
              (change)="toggleCalendars($event, cal.id)"
            ></mbsc-switch>
          </ng-container>
        </div>
        <div class="mbsc-form-group-inset">
          <mbsc-button class="mbsc-button-block" (click)="logOut()">Log out of my account </mbsc-button>
        </div>
      </div>
    </ng-container>
  </div>
  <div class="md-sync-events-google-calendar" [ngClass]="{'md-loading-events': isLoading}">
    <div class="md-sync-events-overlay"></div>
    <mbsc-eventcalendar
      [options]="calendarOptions"
      [data]="myEvents"
      [exclusiveEndDates]="true"
      [clickToCreate]="isEditable"
      [dragToCreate]="isEditable"
      [dragToMove]="isEditable"
      [dragToResize]="isEditable"
      [headerTemplate]="customTemplate"
    >
      <ng-template #customTemplate>
        <mbsc-calendar-nav></mbsc-calendar-nav>
        <div class="md-spinner">
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
          <div class="md-spinner-blade"></div>
        </div>
        <div class="md-google-calendar-header">
          <mbsc-calendar-prev></mbsc-calendar-prev>
          <mbsc-calendar-today></mbsc-calendar-today>
          <mbsc-calendar-next></mbsc-calendar-next>
        </div>
      </ng-template>
    </mbsc-eventcalendar>
  </div>
</mbsc-page>
```

### CSS

```css
.md-sync-events-google-cont.mbsc-page {
  display: flex;
  height: 600px;
}

.md-sync-events-google-menu {
  flex: 0 0 350px;
  height: 100%;
  overflow: auto;
}

.md-sync-events-google-calendar {
  flex: 1 1 auto;
  border-left: 1px solid #ccc;
}

.md-google-calendar-header {
  flex: 1 0 auto;
  display: flex;
  justify-content: flex-end;
}

button.md-sync-events-google-button {
  padding: 8px 24px 8px 50px;
  line-height: 24px;
  background: url(https://cdn.cdnlogo.com/logos/g/35/google-icon.svg) no-repeat;
  background-size: 18px 18px;
  background-position: 11px 11px;
  background-color: #fff;
  color: #757575;
  font-family: Roboto, arial, sans-serif;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  border-radius: 2px;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25);
  transition:
    background-color 0.218s,
    border-color 0.218s,
    box-shadow 0.218s;
}

.md-sync-events-google-button:active {
  background-color: #eee;
  color: #6d6d6d;
}

.md-sync-events-google-button:hover,
.md-sync-events-google-button:focus {
  box-shadow: 0 0 3px 3px rgb(66, 133, 244, 0.3);
}

.md-sync-events-google-inset {
  margin-bottom: 0;
}

@media (max-width: 800px) {
  .md-sync-events-google-cont.mbsc-page {
    display: block;
    height: auto;
  }
}

/* loading spinner and overlay */

.md-loading-events .md-sync-events-overlay {
  position: absolute;
  z-index: 2;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
}

.md-spinner {
  visibility: hidden;
  position: relative;
  width: 20px;
  height: 20px;
}

.md-loading-events .md-spinner {
  visibility: visible;
}

.md-spinner .md-spinner-blade {
  position: absolute;
  left: 44.5%;
  top: 37%;
  width: 10%;
  height: 25%;
  border-radius: 50%/20%;
  background-color: #8c8c8c;
  -webkit-animation: md-spinner-fade 1s linear infinite;
  animation: md-spinner-fade 1s linear infinite;
  -webkit-animation-play-state: paused;
  animation-play-state: paused;
}

.md-spinner .md-spinner-blade:nth-child(1) {
  -webkit-animation-delay: -1.66667s;
  animation-delay: -1.66667s;
  -webkit-transform: rotate(30deg) translate(0, -150%);
  transform: rotate(30deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(2) {
  -webkit-animation-delay: -1.58333s;
  animation-delay: -1.58333s;
  -webkit-transform: rotate(60deg) translate(0, -150%);
  transform: rotate(60deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(3) {
  -webkit-animation-delay: -1.5s;
  animation-delay: -1.5s;
  -webkit-transform: rotate(90deg) translate(0, -150%);
  transform: rotate(90deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(4) {
  -webkit-animation-delay: -1.41667s;
  animation-delay: -1.41667s;
  -webkit-transform: rotate(120deg) translate(0, -150%);
  transform: rotate(120deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(5) {
  -webkit-animation-delay: -1.33333s;
  animation-delay: -1.33333s;
  -webkit-transform: rotate(150deg) translate(0, -150%);
  transform: rotate(150deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(6) {
  -webkit-animation-delay: -1.25s;
  animation-delay: -1.25s;
  -webkit-transform: rotate(180deg) translate(0, -150%);
  transform: rotate(180deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(7) {
  -webkit-animation-delay: -1.16667s;
  animation-delay: -1.16667s;
  -webkit-transform: rotate(210deg) translate(0, -150%);
  transform: rotate(210deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(8) {
  -webkit-animation-delay: -1.08333s;
  animation-delay: -1.08333s;
  -webkit-transform: rotate(240deg) translate(0, -150%);
  transform: rotate(240deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(9) {
  -webkit-animation-delay: -1s;
  animation-delay: -1s;
  -webkit-transform: rotate(270deg) translate(0, -150%);
  transform: rotate(270deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(10) {
  -webkit-animation-delay: -0.91667s;
  animation-delay: -0.91667s;
  -webkit-transform: rotate(300deg) translate(0, -150%);
  transform: rotate(300deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(11) {
  -webkit-animation-delay: -0.83333s;
  animation-delay: -0.83333s;
  -webkit-transform: rotate(330deg) translate(0, -150%);
  transform: rotate(330deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(12) {
  -webkit-animation-delay: -0.75s;
  animation-delay: -0.75s;
  -webkit-transform: rotate(360deg) translate(0, -150%);
  transform: rotate(360deg) translate(0, -150%);
}

.md-loading-events .md-spinner-blade {
  -webkit-animation-play-state: running;
  animation-play-state: running;
}

@-webkit-keyframes md-spinner-fade {
  0% {
    opacity: 0.85;
  }
  50% {
    opacity: 0.25;
  }
  100% {
    opacity: 0.25;
  }
}

@keyframes md-spinner-fade {
  0% {
    opacity: 0.85;
  }
  50% {
    opacity: 0.25;
  }
  100% {
    opacity: 0.25;
  }
}
```

## Implementation notes

- Install `@mobiscroll/calendar-integration` and import `googleCalendarSync`. Call `googleCalendarSync.init({ apiKey: '<YOUR_GOOGLE_API_KEY>', clientId: '<YOUR_GOOGLE_CLIENT_ID>', onSignedIn, onSignedOut })` on component mount.
- Use `view: { calendar: { labels: true } }` with `exclusiveEndDates: true`. Load events on each page navigation via `onPageLoading` (debounced ~200 ms): call `googleCalendarSync.getEvents(calendarIds, viewStart, viewEnd)` and update the events array.
- Layout: `Page` wrapper with a left side panel (auth/calendar controls) and right calendar area. **Signed-out** panel: explanatory text + "Sign in with Google" button (calls `googleCalendarSync.signIn()`). **Signed-in** panel: usage note, "Enable editing" `Switch`, "My Calendars" section with one `Switch` per calendar (toggle fires `googleCalendarSync.getEvents()` for the current range), and "Log out" button (calls `googleCalendarSync.signOut()`).
- On `onSignedIn`: call `googleCalendarSync.getCalendars()`, sort to put primary calendar first, build a `calendarData` map keyed by calendar ID, set `calendarIds` to `[primaryCalendarId]`, then fetch events for the current view range.
- CRUD - all gated by the `editable` switch and `googleCalendarSync.isSignedIn()`: **create** calls `googleCalendarSync.addEvent(primaryCalendarId, event)` in `onEventCreate`; **update** calls `googleCalendarSync.updateEvent(calendarId, event)` in `onEventUpdate` after a `Confirm` dialog; **delete** calls `googleCalendarSync.deleteEvent(calendarId, event)` in `onEventDelete` after a `Confirm` dialog. On API failure, revert the events array to the previous state. Show a `Toast` on each success or error. Pass `clickToCreate`, `dragToCreate`, `dragToMove`, `dragToResize` all bound to the `editable` flag. Set `extendDefaultEvent` to assign the primary calendar's color to newly created events. Use `renderHeader` (Angular: `headerTemplate`, Vue: `#header` slot) to show a CSS-animated loading spinner alongside `CalendarNav`, `CalendarPrev`, `CalendarToday`, `CalendarNext`.

## Run it in your project

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/angular/getting-started/installation

## Related demos

- Next: [Outlook Integration](https://demo.mobiscroll.com/angular/eventcalendar/sync-events-outlook-calendar.md) - Connect the Angular event calendar to Outlook via plugin — sign in users, load calendars, and add, edit, or delete events with optional write-back.
