---
title: "Vue Event calendar Outlook Integration Example | Mobiscroll"
component: "Event calendar"
framework: "Vue"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/vue/eventcalendar/sync-events-outlook-calendar"
---

# Outlook Integration - Vue Event calendar demo

Connect the Vue event calendar to Outlook via plugin — sign in users, load calendars, and add, edit, or delete events with optional write-back. For Vue and Nuxt.

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

## Description

The event calendar comes with third party calendar integration support through the [**integration plugin**](https://mobiscroll.com/docs/vue/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 Outlook 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/vue/eventcalendar/calendar-integrations) to connect to and load events from Outlook Calendars 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 Outlook Calendar account in a desktop month view.
- **Initial state** The month grid is visible with no events loaded by default.
- **Side panel** A separate panel on the left invites the user to sign in with the text `Log into your Outlook account to view and edit your Outlook Calendar events` and a `Sync My Outlook Calendar` button.
- **Authentication flow** Clicking `Sync My Outlook Calendar` opens a new browser tab where the user signs in to their Microsoft account.
- **Connected state** After sign-in, the side panel updates to show the text `Editing events sync back to your calendar when enabled. You'll be asked for confirmation on every action.`
- **Editing toggle** The connected panel includes an `Enable editing` switch that is off by default.
- **Editing toggle enabled** Editing and 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 Outlook 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 Outlook Calendar] calendar` toast appears at the bottom center of the calendar.
- **Calendar selection** The connected panel includes a `My Calendars` section with separate switches for the available Outlook calendars.
- **Sign-out** The bottom of the side panel includes a `Log Out Of My Account` button for disconnecting the Outlook account.
- **Event rendering** Once Outlook events are loaded, day cells display event labels, and the labels can have different visual styles based on the type of event.
- **Event selection** Hovering over or clicking an event label highlights the selected event.
- **Day hover state** Hovering over a day cell highlights the day number in the top-right corner with a gray background.
- **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

- **User-controlled calendar sync** Experiences where users need to connect their own Outlook account, choose which calendars to display, and disconnect when needed.
- **Optional write-back workflows** Scenarios where external calendar events should stay read-only by default, with editing enabled only when the user explicitly turns it on.
- **Multi-calendar visibility** Interfaces that need to surface multiple Outlook calendars, such as personal calendars, shared calendars, or regional holiday calendars, in a single month view.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-themeVariant) - option
- [`labels`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-labels) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`exclusiveEndDates`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-exclusiveEndDates) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-extendDefaultEvent) - option
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToResize) - option
- [`page-loading`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onPageLoading) - event
- [`event-create`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventCreate) - event
- [`event-update`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventUpdate) - event
- [`event-deleted`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventDeleted) - event
- [`close`](https://mobiscroll.com/docs/vue/notifications/toast#event-onClose) - event
- [`close`](https://mobiscroll.com/docs/vue/notifications/confirm#event-onClose) - event

## Code

### SFC JS

```vue
<script setup>
import { outlookCalendarSync } from '@mobiscroll/calendar-integration'
import {
  MbscButton,
  MbscCalendarNav,
  MbscCalendarNext,
  MbscCalendarPrev,
  MbscCalendarToday,
  MbscConfirm,
  MbscEventcalendar,
  MbscPage,
  MbscSwitch,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'

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

const myEvents = ref([])
const isEditable = ref(false)
const isLoggedIn = ref(false)
const isLoading = ref(false)
const isHidden = ref(true)
const calendarIds = ref([])
const myCalendars = ref([])
const startDate = ref(null)
const endDate = ref(null)
const debounce = ref(null)
const calendarData = ref({})
const primaryCalendarId = ref('')
const toastMessage = ref('')
const isToastOpen = ref(false)
const confirmTitle = ref('')
const confirmMessage = ref('')
const confirmOkText = ref('')
const isConfirmOpen = ref(false)
const confirmCallback = ref(null)

const myView = {
  calendar: { labels: true }
}

function myDefaultEvent() {
  return { color: calendarData.value[primaryCalendarId.value].color }
}

function onError(resp) {
  toastMessage.value = resp.error ? resp.error : resp.result.error.message
  isToastOpen.value = true
}

function toggleCalendars(ev, calendarId) {
  const checked = ev.target.checked
  calendarData.value[calendarId].checked = checked

  if (checked) {
    isLoading.value = true
    calendarIds.value = [...calendarIds.value, calendarId]
    outlookCalendarSync
      .getEvents([calendarId], startDate.value, endDate.value)
      .then((events) => {
        isLoading.value = false
        myEvents.value = [...myEvents.value, ...events]
      })
      .catch(onError)
  } else {
    calendarIds.value = calendarIds.value.filter((id) => id !== calendarId)
    myEvents.value = myEvents.value.filter((item) => item.outlookCalendarId !== calendarId)
  }
}

function logOut() {
  outlookCalendarSync.signOut().catch((error) => {
    onError(error)
  })
}

function logIn() {
  if (!outlookCalendarSync.isSignedIn()) {
    outlookCalendarSync.signIn().catch((error) => {
      onError(error)
    })
  }
}

function handlePageLoading(args) {
  clearTimeout(debounce.value)
  startDate.value = args.viewStart
  endDate.value = args.viewEnd
  debounce.value = setTimeout(() => {
    if (outlookCalendarSync.isSignedIn()) {
      isLoading.value = true
      outlookCalendarSync
        .getEvents(calendarIds.value, startDate.value, endDate.value)
        .then((events) => {
          isLoading.value = false
          myEvents.value = events
        })
        .catch(onError)
    }
  }, 200)
}

function handleEventCreate(args) {
  if (outlookCalendarSync.isSignedIn()) {
    const event = args.event

    outlookCalendarSync
      .addEvent(primaryCalendarId.value, event)
      .then((newEvent) => {
        newEvent.color = event.color
        myEvents.value = [...myEvents.value.filter((item) => item.id !== event.id), newEvent]
        toastMessage.value =
          'Event created in "' + calendarData.value[primaryCalendarId.value].name + '" calendar'
        isToastOpen.value = true
      })
      .catch((error) => {
        myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
        onError(error)
      })
  }
}
function handleEventUpdate(args) {
  if (outlookCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to update this event?'
    confirmMessage.value = 'This action will affect your Outlook Calendar event.'
    confirmOkText.value = 'Update'
    confirmCallback.value = function (result) {
      const event = args.event
      if (result) {
        const calendarId = event.outlookCalendarId
        outlookCalendarSync
          .updateEvent(calendarId, event)
          .then(() => {
            myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
            toastMessage.value =
              'Event updated on "' + calendarData.value[calendarId].name + '" calendar'
            isToastOpen.value = true
          })
          .catch((error) => {
            myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
            myEvents.value = [
              ...myEvents.value.filter((item) => item.id !== event.id),
              args.oldEvent
            ]
            onError(error)
          })
      } else {
        myEvents.value = [...myEvents.value.filter((item) => item.id !== event.id), args.oldEvent]
      }
    }
  }
  isConfirmOpen.value = true
}
function handleEventDelete(args) {
  if (outlookCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to delete this event?'
    confirmMessage.value = 'This action will remove the event from your Outlook Calendar as well.'
    confirmOkText.value = 'Delete'
    confirmCallback.value = function (result) {
      if (result) {
        const event = args.event
        const calendarId = event.outlookCalendarId
        outlookCalendarSync
          .deleteEvent(calendarId, event)
          .then(() => {
            myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
            toastMessage.value =
              'Event deleted from "' + calendarData.value[calendarId].name + '" calendar'
            isToastOpen.value = true
          })
          .catch(onError)
      }
    }
    isConfirmOpen.value = true
  }
  return false
}

function handleToastClose() {
  isToastOpen.value = false
}

function handleConfirmClose() {
  isConfirmOpen.value = false
}

onMounted(() => {
  function onSignedIn() {
    isLoggedIn.value = true
    outlookCalendarSync
      .getCalendars()
      .then((calendars) => {
        calendars.sort((c) => (c.isDefaultCalendar ? -1 : 1))

        const calData = {}
        const primaryCalId = calendars[0].id

        for (const c of calendars) {
          calData[c.id] = {
            name: c.name,
            color: c.hexColor,
            checked: c.id === primaryCalId
          }
        }

        calendarIds.value = [primaryCalId]
        primaryCalendarId.value = primaryCalId
        calendarData.value = calData
        myCalendars.value = calendars
        isLoading.value = true

        return outlookCalendarSync.getEvents([primaryCalId], startDate.value, endDate.value)
      })
      .then((events) => {
        myEvents.value = events
        isLoading.value = false
      })
      .catch(onError)
  }

  const onSignedOut = () => {
    isLoggedIn.value = false
    myCalendars.value = []
    calendarIds.value = []
    calendarData.value = {}
    myEvents.value = []
  }

  isHidden.value = false

  // Init outlook client
  outlookCalendarSync.init({
    clientId: '<YOUR_OUTLOOK_CLIENT_ID>',
    redirectUri: '<YOUR_OUTLOOK_REDIRECT_URI>',
    onSignedIn: onSignedIn,
    onSignedOut: onSignedOut
  })
})
</script>

<template>
  <MbscPage className="md-sync-events-outlook-cont" :class="{ 'md-loading-events': isLoading }">
    <div :class="{ 'md-sync-events-outlook-menu': true, 'mbsc-hidden': isHidden }">
      <template v-if="!isLoggedIn">
        <div class="mbsc-form-group-inset mbsc-align-center" :aria-hidden="isLoggedIn">
          <p class="mbsc-italic mbsc-txt-muted">
            Log into your Outlook account to view and edit your Outlook Calendar events
          </p>
          <MbscButton @click="logIn()" className="mbsc-reset md-sync-events-outlook-button">
            Sign in with Outlook
          </MbscButton>
        </div>
      </template>
      <template v-if="isLoggedIn">
        <div :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">
            <MbscSwitch label="Enable editing" v-model="isEditable" />
          </div>
          <div class="mbsc-form-group-inset md-sync-events-outlook-inset">
            <template v-for="cal in myCalendars" :key="cal.id">
              <MbscSwitch
                :label="cal.name"
                v-model="calendarData[cal.id].checked"
                @change="toggleCalendars($event, cal.id)"
              />
            </template>
          </div>
          <div class="mbsc-form-group-inset">
            <MbscButton className="mbsc-button-block" @click="logOut()">
              Log out of my account
            </MbscButton>
          </div>
        </div>
      </template>
    </div>
    <div class="md-sync-events-outlook-calendar" :class="{ 'md-loading-events': isLoading }">
      <div class="md-sync-events-overlay"></div>
      <MbscEventcalendar
        :view="myView"
        :data="myEvents"
        :exclusiveEndDates="true"
        :extendDefaultEvent="myDefaultEvent"
        :clickToCreate="isEditable"
        :dragToCreate="isEditable"
        :dragToMove="isEditable"
        :dragToResize="isEditable"
        @page-loading="handlePageLoading"
        @event-create="handleEventCreate"
        @event-update="handleEventUpdate"
        @event-deleted="handleEventDelete"
      >
        <template #header>
          <MbscCalendarNav />
          <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-outlook-calendar-header">
            <MbscCalendarPrev />
            <MbscCalendarToday />
            <MbscCalendarNext />
          </div>
        </template>
      </MbscEventcalendar>
    </div>
  </MbscPage>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="handleToastClose" />
  <MbscConfirm
    :title="confirmTitle"
    :message="confirmMessage"
    :okText="confirmOkText"
    :isOpen="isConfirmOpen"
    :callback="confirmCallback"
    @close="handleConfirmClose"
  />
</template>

<style>
.md-sync-events-outlook-cont.mbsc-page {
  display: flex;
  height: 600px;
}

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

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

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

.md-sync-events-outlook-button.mbsc-button {
  text-transform: capitalize;
}

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

@media (max-width: 800px) {
  .md-sync-events-outlook-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;
  }
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import { outlookCalendarSync } from '@mobiscroll/calendar-integration'
import {
  MbscButton,
  MbscCalendarNav,
  MbscCalendarNext,
  MbscCalendarPrev,
  MbscCalendarToday,
  MbscConfirm,
  MbscEventcalendar,
  MbscPage,
  MbscSwitch,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import type {
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscEventCreateEvent,
  MbscEventDeleteEvent,
  MbscEventUpdateEvent,
  MbscPageLoadingEvent
} from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'

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

const myEvents = ref<MbscCalendarEvent[]>([])
const isEditable = ref<boolean>(false)
const isLoggedIn = ref<boolean>(false)
const isLoading = ref<boolean>(false)
const isHidden = ref<boolean>(true)
const calendarIds = ref<any>([])
const myCalendars = ref<any>([])
const startDate = ref<any>(null)
const endDate = ref<any>(null)
const debounce = ref<any>(null)
const calendarData = ref<any>({})
const primaryCalendarId = ref<string>('')
const toastMessage = ref<string>('')
const isToastOpen = ref<boolean>(false)
const confirmTitle = ref<string>('')
const confirmMessage = ref<string>('')
const confirmOkText = ref<string>('')
const isConfirmOpen = ref<boolean>(false)
const confirmCallback = ref<any>(null)

const myView: MbscEventcalendarView = {
  calendar: { labels: true }
}

function myDefaultEvent() {
  return { color: calendarData.value[primaryCalendarId.value].color }
}

function onError(resp: any) {
  toastMessage.value = resp.error ? resp.error : resp.result.error.message
  isToastOpen.value = true
}

function toggleCalendars(ev: any, calendarId: string) {
  const checked = ev.target.checked
  calendarData.value[calendarId].checked = checked

  if (checked) {
    isLoading.value = true
    calendarIds.value = [...calendarIds.value, calendarId]
    outlookCalendarSync
      .getEvents([calendarId], startDate.value, endDate.value)
      .then((events) => {
        isLoading.value = false
        myEvents.value = [...myEvents.value, ...events]
      })
      .catch(onError)
  } else {
    calendarIds.value = calendarIds.value.filter((id: string) => id !== calendarId)
    myEvents.value = myEvents.value.filter((item) => item.outlookCalendarId !== calendarId)
  }
}

function logOut() {
  outlookCalendarSync.signOut().catch((error: any) => {
    onError(error)
  })
}

function logIn() {
  if (!outlookCalendarSync.isSignedIn()) {
    outlookCalendarSync.signIn().catch((error: any) => {
      onError(error)
    })
  }
}

function handlePageLoading(args: MbscPageLoadingEvent) {
  clearTimeout(debounce.value)
  startDate.value = args.viewStart
  endDate.value = args.viewEnd
  debounce.value = setTimeout(() => {
    if (outlookCalendarSync.isSignedIn()) {
      isLoading.value = true
      outlookCalendarSync
        .getEvents(calendarIds.value, startDate.value, endDate.value)
        .then((events) => {
          isLoading.value = false
          myEvents.value = events
        })
        .catch(onError)
    }
  }, 200)
}

function handleEventCreate(args: MbscEventCreateEvent) {
  if (outlookCalendarSync.isSignedIn()) {
    const event = args.event

    outlookCalendarSync
      .addEvent(primaryCalendarId.value, event)
      .then((newEvent: MbscCalendarEvent) => {
        newEvent.color = event.color
        myEvents.value = [...myEvents.value.filter((item) => item.id !== event.id), newEvent]
        toastMessage.value =
          'Event created in "' + calendarData.value[primaryCalendarId.value].name + '" calendar'
        isToastOpen.value = true
      })
      .catch((error: any) => {
        myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
        onError(error)
      })
  }
}
function handleEventUpdate(args: MbscEventUpdateEvent) {
  if (outlookCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to update this event?'
    confirmMessage.value = 'This action will affect your Outlook Calendar event.'
    confirmOkText.value = 'Update'
    confirmCallback.value = function (result: any) {
      const event = args.event
      if (result) {
        const calendarId = event.outlookCalendarId
        outlookCalendarSync
          .updateEvent(calendarId, event)
          .then(() => {
            myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
            toastMessage.value =
              'Event updated on "' + calendarData.value[calendarId].name + '" calendar'
            isToastOpen.value = true
          })
          .catch((error: any) => {
            myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
            myEvents.value = [
              ...myEvents.value.filter((item) => item.id !== event.id),
              args.oldEvent!
            ]
            onError(error)
          })
      } else {
        myEvents.value = [...myEvents.value.filter((item) => item.id !== event.id), args.oldEvent!]
      }
    }
  }
  isConfirmOpen.value = true
}
function handleEventDelete(args: MbscEventDeleteEvent) {
  if (outlookCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to delete this event?'
    confirmMessage.value = 'This action will remove the event from your Outlook Calendar as well.'
    confirmOkText.value = 'Delete'
    confirmCallback.value = function (result: any) {
      if (result) {
        const event = args.event
        const calendarId = event.outlookCalendarId
        outlookCalendarSync
          .deleteEvent(calendarId, event)
          .then(() => {
            myEvents.value = myEvents.value.filter((item) => item.id !== event.id)
            toastMessage.value =
              'Event deleted from "' + calendarData.value[calendarId].name + '" calendar'
            isToastOpen.value = true
          })
          .catch(onError)
      }
    }
    isConfirmOpen.value = true
  }
  return false
}

function handleToastClose() {
  isToastOpen.value = false
}

function handleConfirmClose() {
  isConfirmOpen.value = false
}

onMounted(() => {
  function onSignedIn() {
    isLoggedIn.value = true
    outlookCalendarSync
      .getCalendars()
      .then((calendars: any) => {
        calendars.sort((c: any) => (c.isDefaultCalendar ? -1 : 1))

        const calData: any = {}
        const primaryCalId = calendars[0].id

        for (const c of calendars) {
          calData[c.id] = {
            name: c.name,
            color: c.hexColor,
            checked: c.id === primaryCalId
          }
        }

        calendarIds.value = [primaryCalId]
        primaryCalendarId.value = primaryCalId
        calendarData.value = calData
        myCalendars.value = calendars
        isLoading.value = true

        return outlookCalendarSync.getEvents([primaryCalId], startDate.value, endDate.value)
      })
      .then((events: any) => {
        myEvents.value = events
        isLoading.value = false
      })
      .catch(onError)
  }

  const onSignedOut = () => {
    isLoggedIn.value = false
    myCalendars.value = []
    calendarIds.value = []
    calendarData.value = {}
    myEvents.value = []
  }

  isHidden.value = false

  // Init outlook client
  outlookCalendarSync.init({
    clientId: '<YOUR_OUTLOOK_CLIENT_ID>',
    redirectUri: '<YOUR_OUTLOOK_REDIRECT_URI>',
    onSignedIn: onSignedIn,
    onSignedOut: onSignedOut
  })
})
</script>

<template>
  <MbscPage className="md-sync-events-outlook-cont" :class="{ 'md-loading-events': isLoading }">
    <div :class="{ 'md-sync-events-outlook-menu': true, 'mbsc-hidden': isHidden }">
      <template v-if="!isLoggedIn">
        <div class="mbsc-form-group-inset mbsc-align-center" :aria-hidden="isLoggedIn">
          <p class="mbsc-italic mbsc-txt-muted">
            Log into your Outlook account to view and edit your Outlook Calendar events
          </p>
          <MbscButton @click="logIn()" className="mbsc-reset md-sync-events-outlook-button">
            Sign in with Outlook
          </MbscButton>
        </div>
      </template>
      <template v-if="isLoggedIn">
        <div :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">
            <MbscSwitch label="Enable editing" v-model="isEditable" />
          </div>
          <div class="mbsc-form-group-inset md-sync-events-outlook-inset">
            <template v-for="cal in myCalendars" :key="cal.id">
              <MbscSwitch
                :label="cal.name"
                v-model="calendarData[cal.id].checked"
                @change="toggleCalendars($event, cal.id)"
              />
            </template>
          </div>
          <div class="mbsc-form-group-inset">
            <MbscButton className="mbsc-button-block" @click="logOut()">
              Log out of my account
            </MbscButton>
          </div>
        </div>
      </template>
    </div>
    <div class="md-sync-events-outlook-calendar" :class="{ 'md-loading-events': isLoading }">
      <div class="md-sync-events-overlay"></div>
      <MbscEventcalendar
        :view="myView"
        :data="myEvents"
        :exclusiveEndDates="true"
        :extendDefaultEvent="myDefaultEvent"
        :clickToCreate="isEditable"
        :dragToCreate="isEditable"
        :dragToMove="isEditable"
        :dragToResize="isEditable"
        @page-loading="handlePageLoading"
        @event-create="handleEventCreate"
        @event-update="handleEventUpdate"
        @event-deleted="handleEventDelete"
      >
        <template #header>
          <MbscCalendarNav />
          <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-outlook-calendar-header">
            <MbscCalendarPrev />
            <MbscCalendarToday />
            <MbscCalendarNext />
          </div>
        </template>
      </MbscEventcalendar>
    </div>
  </MbscPage>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="handleToastClose" />
  <MbscConfirm
    :title="confirmTitle"
    :message="confirmMessage"
    :okText="confirmOkText"
    :isOpen="isConfirmOpen"
    :callback="confirmCallback"
    @close="handleConfirmClose"
  />
</template>

<style>
.md-sync-events-outlook-cont.mbsc-page {
  display: flex;
  height: 600px;
}

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

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

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

.md-sync-events-outlook-button.mbsc-button {
  text-transform: capitalize;
}

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

@media (max-width: 800px) {
  .md-sync-events-outlook-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;
  }
}
</style>
```

## Implementation notes

- Use `view: { calendar: { labels: true } }` with `exclusiveEndDates: true`. Start with empty `data` - no resources are used; all calendar events appear in a single view.
- Import `outlookCalendarSync` from `@mobiscroll/calendar-integration` (a separately installed package).
- Call `outlookCalendarSync.init()` on mount with `clientId`, `redirectUri`, `onSignedIn`, and `onSignedOut`.
- **`onSignedIn`**: call `outlookCalendarSync.getCalendars()` and sort so the default calendar appears first (`c.isDefaultCalendar ? -1 : 1`). Build a `calendarData` map keyed by `c.id` storing `name` (`c.name`), `color` (`c.hexColor`), and `checked`. Set `calendarIds` to `[primaryCalId]` initially, then call `getEvents([primaryCalId], startDate, endDate)`. Angular: wrap `.then()` callbacks in `NgZone.run()`.
- **`onSignedOut`**: clear events, calendarIds, calendarData, and calendars list.
- **`onPageLoading`**: debounce by 200ms; if `outlookCalendarSync.isSignedIn()`, call `getEvents(calendarIds, args.viewStart, args.viewEnd)` and replace events. Store `args.viewStart`/`args.viewEnd` in refs for use in toggle handlers.
- **Toggle calendar** (per-calendar Switch in sidebar): enabling - push to `calendarIds`, call `getEvents([calendarId], startDate, endDate)`, append returned events; disabling - filter calendarId and events out by `event.outlookCalendarId`.
- Set `extendDefaultEvent` to return `{ color: calendarData[primaryCalendarId].color }` so newly created events use the primary calendar's color.
- **CRUD** - all four options (`clickToCreate`, `dragToCreate`, `dragToMove`, `dragToResize`) controlled by an "Enable editing" Switch (off by default):
  - **Create** (`onEventCreate`): call `outlookCalendarSync.addEvent(primaryCalendarId, event)`. On success, set the new event's color and append it; on failure, show an error toast.
  - **Update** (`onEventUpdate`): `Confirm` dialog ("Are you sure you want to update this event?" / "This action will affect your Outlook Calendar event." / "Update") → `outlookCalendarSync.updateEvent(event.outlookCalendarId, event)`. On cancel or failure, restore `args.oldEvent`.
  - **Delete** (`onEventDelete`): return `false`, `Confirm` dialog ("Delete") → `outlookCalendarSync.deleteEvent(event.outlookCalendarId, event)`, filter the event out on success.
- **Layout**: a persistent sidebar panel alongside the calendar. When not logged in: "Sync my outlook calendars" sign-in button. When logged in: editing note, "Enable editing" Switch, per-calendar Switches (label: `cal.name`), "Log out" button.
- **Custom header** (`renderHeader`, Angular: `headerTemplate`, Vue: `#header` slot): `CalendarNav` (left), loading spinner (visible while events load), then `CalendarPrev`, `CalendarToday`, `CalendarNext` (right).

## 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/vue/getting-started/installation

## Related demos

- Previous: [Google Calendar Sync](https://demo.mobiscroll.com/vue/eventcalendar/sync-events-google-calendar.md) - Connect Google Calendar to a Vue event calendar — authenticate users, load events, and sync create, edit, and delete actions to Google Calendar.
- Next: [Public Google Calendar Integration](https://demo.mobiscroll.com/vue/eventcalendar/load-events-from-google-calendar.md) - Load events from a public Google Calendar into a Vue event calendar using the integration plugin — provide your Calendar ID and API key.
