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

# Google Calendar Sync - Vue Event calendar demo

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

- [Live demo](https://demo.mobiscroll.com/vue/eventcalendar/sync-events-google-calendar)
- [View SFC JS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue/tree/main/src/demos/eventcalendar/calendar-view/sync-events-google-calendar)
- [View SFC TS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue-ts/tree/main/src/demos/eventcalendar/calendar-view/sync-events-google-calendar)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/eventcalendar/sync-events-google-calendar.md), [jQuery](https://demo.mobiscroll.com/jquery/eventcalendar/sync-events-google-calendar.md), [Angular](https://demo.mobiscroll.com/angular/eventcalendar/sync-events-google-calendar.md), [React](https://demo.mobiscroll.com/react/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/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 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/vue/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/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 { googleCalendarSync } 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) {
    calendarIds.value = [...calendarIds.value, calendarId]
  } else {
    calendarIds.value = calendarIds.value.filter((id) => id !== calendarId)
  }
  if (calendarIds.value.length === 0) {
    myEvents.value = []
    return
  }
  isLoading.value = true
  googleCalendarSync
    .getEvents(calendarIds.value, startDate.value, endDate.value)
    .then((events) => {
      isLoading.value = false
      myEvents.value = events
    })
    .catch(onError)
}

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

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

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

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

    googleCalendarSync
      .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 (googleCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to update this event?'
    confirmMessage.value = 'This action will affect your Google Calendar event.'
    confirmOkText.value = 'Update'
    confirmCallback.value = function (result) {
      const event = args.event
      if (result) {
        const calendarId = event.googleCalendarId
        googleCalendarSync
          .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 (googleCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to delete this event?'
    confirmMessage.value = 'This action will remove the event from your Google Calendar as well.'
    confirmOkText.value = 'Delete'
    confirmCallback.value = function (result) {
      if (result) {
        const event = args.event
        const calendarId = event.googleCalendarId
        googleCalendarSync
          .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
    googleCalendarSync
      .getCalendars()
      .then((calendars) => {
        calendars.sort((c) => (c.primary ? -1 : 1))

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

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

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

        return googleCalendarSync.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 google client
  googleCalendarSync.init({
    apiKey: '<YOUR_GOOGLE_API_KEY>',
    clientId: '<YOUR_GOOGLE_CLIENT_ID>',
    onSignedIn: onSignedIn,
    onSignedOut: onSignedOut
  })
})
</script>

<template>
  <MbscPage className="md-sync-events-google-cont" :class="{ 'md-loading-events': isLoading }">
    <div :class="{ 'md-sync-events-google-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 Google account to view and edit your Google Calendar events
          </p>
          <MbscButton @click="logIn()" className="mbsc-reset md-sync-events-google-button">
            Sign in with Google
          </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-google-inset">
            <template v-for="cal in myCalendars" :key="cal.id">
              <MbscSwitch
                :label="cal.summary"
                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-google-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-google-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-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;
  }
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import { googleCalendarSync } 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) {
    calendarIds.value = [...calendarIds.value, calendarId]
  } else {
    calendarIds.value = calendarIds.value.filter((id: string) => id !== calendarId)
  }
  if (calendarIds.value.length === 0) {
    myEvents.value = []
    return
  }
  isLoading.value = true
  googleCalendarSync
    .getEvents(calendarIds.value, startDate.value, endDate.value)
    .then((events) => {
      isLoading.value = false
      myEvents.value = events
    })
    .catch(onError)
}

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

function logIn() {
  if (!googleCalendarSync.isSignedIn()) {
    googleCalendarSync.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 (googleCalendarSync.isSignedIn()) {
      isLoading.value = true
      googleCalendarSync
        .getEvents(calendarIds.value, startDate.value, endDate.value)
        .then((events) => {
          isLoading.value = false
          myEvents.value = events
        })
        .catch(onError)
    }
  }, 200)
}

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

    googleCalendarSync
      .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 (googleCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to update this event?'
    confirmMessage.value = 'This action will affect your Google Calendar event.'
    confirmOkText.value = 'Update'
    confirmCallback.value = function (result: any) {
      const event = args.event
      if (result) {
        const calendarId = event.googleCalendarId
        googleCalendarSync
          .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 (googleCalendarSync.isSignedIn()) {
    confirmTitle.value = 'Are you sure you want to delete this event?'
    confirmMessage.value = 'This action will remove the event from your Google Calendar as well.'
    confirmOkText.value = 'Delete'
    confirmCallback.value = function (result: any) {
      if (result) {
        const event = args.event
        const calendarId = event.googleCalendarId
        googleCalendarSync
          .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
    googleCalendarSync
      .getCalendars()
      .then((calendars: any) => {
        calendars.sort((c: any) => (c.primary ? -1 : 1))

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

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

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

        return googleCalendarSync.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 google client
  googleCalendarSync.init({
    apiKey: '<YOUR_GOOGLE_API_KEY>',
    clientId: '<YOUR_GOOGLE_CLIENT_ID>',
    onSignedIn: onSignedIn,
    onSignedOut: onSignedOut
  })
})
</script>

<template>
  <MbscPage className="md-sync-events-google-cont" :class="{ 'md-loading-events': isLoading }">
    <div :class="{ 'md-sync-events-google-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 Google account to view and edit your Google Calendar events
          </p>
          <MbscButton @click="logIn()" className="mbsc-reset md-sync-events-google-button">
            Sign in with Google
          </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-google-inset">
            <template v-for="cal in myCalendars" :key="cal.id">
              <MbscSwitch
                :label="cal.summary"
                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-google-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-google-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-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;
  }
}
</style>
```

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

## Related demos

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