CalendarService Example

Here’s a basic example of a Lightning web component that displays calendar events and allows the user to perform basic calendar-related functions.

The component’s HTML template is minimal, with a “main” display view that lists calendar events and a “detail” display view that shows an event’s details.

1<template>
2  <!-- Main View -->
3  <template if:false="{detailViewIsOpen}">
4    <lightning-card title="Today's Events" icon-name="utility:dayview">
5      <template for:each="{todayEvents}" for:item="item">
6        <div
7          class="slds-var-p-horizontal_medium slds-var-p-vertical_x-small"
8          key="{item.id}"
9          onclick="{showDetailView}"
10          data-id="{item.id}"
11        >
12          <p class="slds-text-heading_small"><b>{item.title}</b></p>
13          <p class="slds-text-heading_small">{item.startTimeDisplay} — {item.endTimeDisplay}</p>
14        </div>
15      </template>
16    </lightning-card>
17  </template>
18
19  <!-- Detail View -->
20  <template if:true="{detailViewIsOpen}">
21    <div class="slds-var-p-around_medium" style="background-color: white; border-radius: 4px;">
22      <table>
23        <tbody>
24          <tr>
25            <td class="slds-align-top" width="1">
26              <lightning-icon
27                icon-name="utility:chevronleft"
28                onclick="{hideDetailView}"
29              ></lightning-icon>
30            </td>
31            <td
32              class="sldx-align-top slds-text-heading_medium slds-var-p-bottom_medium slds-align_absolute-center"
33            >
34              Event Details
35            </td>
36            <td class="slds-align-top" width="1">
37              <lightning-button-menu
38                alternative-text="Show menu"
39                variant="border-filled"
40                menu-alignment="auto"
41              >
42                <lightning-menu-item
43                  prefix-icon-name="utility:add"
44                  value="Add"
45                  label="Add to Device Calendar"
46                  onclick="{addCalendarEvent}"
47                ></lightning-menu-item>
48                <lightning-menu-item
49                  prefix-icon-name="utility:edit"
50                  value="Update"
51                  label="Update in Device Calendar"
52                  onclick="{updateCalendarEvent}"
53                ></lightning-menu-item>
54                <lightning-menu-item
55                  prefix-icon-name="utility:delete"
56                  value="Delete"
57                  label="Remove from Device Calendar"
58                  onclick="{deleteCalendarEvent}"
59                ></lightning-menu-item>
60              </lightning-button-menu>
61            </td>
62          </tr>
63          <tr>
64            <td colspan="3">
65              <ul class="slds-has-dividers_bottom-space">
66                <li class="slds-item">
67                  <span class="slds-text-heading_small"><b>{selectedItem.title}</b></span
68                  ><br />
69                  <span class="slds-text-heading_small"
70                    >{selectedItem.startTimeDisplay} — {selectedItem.endTimeDisplay}</span
71                  >
72                </li>
73                <li class="slds-item">
74                  <span class="slds-text-heading_small">Reminders</span><br />
75                  <template for:each="{selectedItem.alarmsDisplay}" for:item="alarm">
76                    <span class="slds-text-body_regular" key="{alarm}">{alarm}<br /></span>
77                  </template>
78                </li>
79                <li class="slds-item">
80                  <span class="slds-text-heading_small">Location</span><br />
81                  <span class="slds-text-body_regular">{selectedItem.location}</span>
82                </li>
83                <li class="slds-item">
84                  <span class="slds-text-heading_small">Attendees</span><br />
85                  <template for:each="{selectedItem.attendees}" for:item="attendee">
86                    <span class="slds-text-body_regular" key="{attendee.name}"
87                      >{attendee.name} ({attendee.email})<br
88                    /></span>
89                  </template>
90                </li>
91                <li class="slds-item">
92                  <span class="slds-text-heading_small">Notes</span><br />
93                  <span class="slds-text-body_regular">{selectedItem.notes}</span>
94                </li>
95              </ul>
96            </td>
97          </tr>
98        </tbody>
99      </table>
100    </div>
101  </template>
102</template>

This example simply uses CalendarService to display events, and allows you to perform simple actions on calendar items. A status message is returned when there’s an error. In this example, the events are hard-coded, rather than fetched via API calls from a Salesforce org. You’ll need to build functionality to fetch event data from your Salesforce org as part of your component.

1import { api, LightningElement } from "lwc";
2import { getCalendarService } from "lightning/mobileCapabilities";
3import LightningAlert from "lightning/alert";
4import LightningConfirm from "lightning/confirm";
5
6export default class CalendarForToday extends LightningElement {
7  todayEvents = [];
8  detailViewIsOpen = false;
9  selectedItem = null;
10  selectedItemIndex = -1;
11  calendarPermissionRationaleText =
12    "Allow access to your calendar to enable calendar event processing.";
13  calendarService;
14
15  connectedCallback() {
16    console.log("Start connected callback");
17
18    try {
19      this.calendarService = getCalendarService();
20      this.todayEvents = this.getTodayEvents();
21      this.todayEvents.forEach((item) => this.generateDisplayFields(item));
22      console.log(`End connected callback with ${this.todayEvents.length} events for today.`);
23    } catch (err) {
24      console.log(`connectedCallback failed with error: ${err}`);
25    }
26  }
27
28  showDetailView(event) {
29    const id = event.currentTarget.dataset.id;
30    this.selectedItemIndex = this.todayEvents.findIndex((item) => item.id === id);
31    if (this.selectedItemIndex != -1) {
32      this.selectedItem = this.todayEvents[this.selectedItemIndex];
33    } else {
34      this.selectedItem = null;
35    }
36    this.detailViewIsOpen = this.selectedItem != null;
37  }
38
39  hideDetailView() {
40    this.detailViewIsOpen = false;
41    this.selectedItem = null;
42    this.selectedItemIndex = -1;
43  }
44
45  addCalendarEvent() {
46    if (this.calendarService.isAvailable() && this.selectedItemIndex != -1 && this.selectedItem) {
47      const options = {
48        permissionRationaleText: this.calendarPermissionRationaleText,
49      };
50
51      console.log(`options: ${JSON.stringify(options)}`);
52      console.log(`Adding selectedItem: ${JSON.stringify(this.selectedItem)}`);
53
54      this.calendarService
55        .addEvent(this.selectedItem, options)
56        .then((sanitizedEvent) => {
57          this.generateDisplayFields(sanitizedEvent);
58          this.selectedItem = sanitizedEvent;
59          this.todayEvents[this.selectedItemIndex] = sanitizedEvent;
60          this.showSuccessAlert(
61            "Add Event",
62            "Event was added successfully to the device default calendar.",
63          );
64          console.log(`sanitizedEvent: ${JSON.stringify(sanitizedEvent)}`);
65        })
66        .catch((error) => {
67          console.error(error);
68          this.showFailureAlert(
69            "Add Event",
70            `There was a problem adding the event to the device default calendar: ${error.message}`,
71          );
72        });
73    } else {
74      console.log("Calendar Service Is Not Available");
75      this.showFailureAlert("Add Event", "Calendar Service is not available.");
76    }
77  }
78
79  updateCalendarEvent() {
80    if (this.calendarService.isAvailable() && this.selectedItemIndex != -1 && this.selectedItem) {
81      // For this sample code, we've hard-coded some trivial changes
82
83      this.selectedItem.title += " - Updated";
84      this.selectedItem.notes += " - Updated";
85
86      const options = {
87        permissionRationaleText: this.calendarPermissionRationaleText,
88        span: "ThisEvent",
89      };
90
91      console.log(`options: ${JSON.stringify(options)}`);
92      console.log(`Updating selectedItem: ${JSON.stringify(this.selectedItem)}`);
93
94      this.calendarService
95        .updateEvent(this.selectedItem, options)
96        .then((sanitizedEvent) => {
97          this.generateDisplayFields(sanitizedEvent);
98          this.selectedItem = sanitizedEvent;
99          this.todayEvents[this.selectedItemIndex] = sanitizedEvent;
100          this.showSuccessAlert(
101            "Update Event",
102            "Event was updated successfully in the device default calendar.",
103          );
104          console.log(`sanitizedEvent: ${JSON.stringify(sanitizedEvent)}`);
105        })
106        .catch((error) => {
107          console.error(error);
108          this.showFailureAlert(
109            "Update Event",
110            `There was a problem updating the event in the device default calendar: ${error.message}`,
111          );
112        });
113    } else {
114      console.log("Calendar Service Is Not Available");
115      this.showFailureAlert("Update Event", "Calendar Service is not available.");
116    }
117  }
118
119  deleteCalendarEvent() {
120    if (this.calendarService.isAvailable() && this.selectedItemIndex != -1 && this.selectedItem) {
121      LightningConfirm.open({
122        label: "Delete Event",
123        message: "Are you sure you want to delete this event?",
124        theme: "warning",
125      }).then((response) => {
126        if (response === true) {
127          const options = {
128            permissionRationaleText: this.calendarPermissionRationaleText,
129            span: "ThisEvent",
130          };
131
132          console.log(`options: ${JSON.stringify(options)}`);
133          console.log(`Deleting selectedItem: ${JSON.stringify(this.selectedItem)}`);
134
135          this.calendarService
136            .removeEvent(this.selectedItem, options)
137            .then(() => {
138              this.todayEvents.splice(this.selectedItemIndex, 1);
139              this.hideDetailView();
140              this.showSuccessAlert(
141                "Delete Event",
142                "Event was removed successfully from the device default calendar.",
143              );
144            })
145            .catch((error) => {
146              console.error(error);
147              this.showFailureAlert(
148                "Delete Event",
149                `There was a problem removing the event from the device default calendar: ${error.message}`,
150              );
151            });
152        }
153      });
154    } else {
155      console.log("Calendar Service Is Not Available");
156      this.showFailureAlert("Delete Event", "Calendar Service is not available.");
157    }
158  }
159
160  getTodayEvents() {
161    // For this sample code, we've hard-coded some made-up values.
162    // Your component should fetch events from your SF org and convert them to the following object format.
163    const events = [
164      {
165        id: "event_id_1", // will be overwritten after a call to calendarService.addEvent()
166        isAllDay: false,
167        startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(8, 0, 0), // 8 AM
168        endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(10, 0, 0), // 10 AM
169        availability: "Busy",
170        status: "Confirmed",
171        calendarId: null, // will be assigned to the default device calendar
172        title: "Team Meeting",
173        location: "3514 Ruckman Road, San Francisco, CA 94105",
174        notes: "Discussing customer request for new calendar feature",
175        alarms: [{ relativeOffsetSeconds: 600 }], // 10 mins before event
176        attendees: [
177          {
178            name: "Jamal Booker",
179            email: "jbooker_fake_email@email.com",
180            role: "Required",
181            status: "Accepted",
182          },
183          {
184            name: "Robert Bullard",
185            email: "bob.bullard.fake.email@email.com",
186            role: "Required",
187            status: "Pending",
188          },
189          {
190            name: "Gordon Chu",
191            email: "gordonchu736251_email@email.com",
192            role: "Optional",
193            status: "Declined",
194          },
195        ],
196        recurrenceRules: null,
197      },
198      {
199        id: "event_id_2", // will be overwritten after a call to calendarService.addEvent()
200        isAllDay: false,
201        startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(10, 30, 0), // 10:30 AM
202        endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(11, 0, 0), // 11 AM
203        availability: "Busy",
204        status: "Confirmed",
205        calendarId: null, // will be assigned to the default device calendar
206        title: "Quarterly Review",
207        location: "2135 Alpha Avenue, Fernandina Beach, FL 32034",
208        notes: "Reviewing results of Q2 and planning Q3",
209        alarms: [{ relativeOffsetSeconds: 1800 }], // 30 mins before event
210        attendees: [
211          {
212            name: "Alex Driskel",
213            email: "adriskell_fake_email@email.com",
214            role: "Required",
215            status: "Accepted",
216          },
217          {
218            name: "Kim Friedman",
219            email: "nothing_is_kimpossible_fake_email@email.com",
220            role: "Required",
221            status: "Tentative",
222          },
223          {
224            name: "April Guthman",
225            email: "guthman.april.fake@email.com",
226            role: "Required",
227            status: "Pending",
228          },
229          {
230            name: "Leif Hansen",
231            email: "leifhansenfakeemail@email.com",
232            role: "Required",
233            status: "Accepted",
234          },
235        ],
236        recurrenceRules: null,
237      },
238      {
239        id: "event_id_3", // will be overwritten after a call to calendarService.addEvent()
240        isAllDay: false,
241        startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(11, 0, 0), // 11 AM
242        endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(12, 0, 0), // 12 PM
243        availability: "Busy",
244        status: "Confirmed",
245        calendarId: null, // will be assigned to the default device calendar
246        title: "Portfolio Checklist",
247        location: "2281 Radford Street, Louisville, KY 40291",
248        notes: "Creating a guide to help sales in compiling a strong portfolio",
249        alarms: [{ relativeOffsetSeconds: 600 }], // 10 mins before event
250        attendees: [
251          {
252            name: "Marie Hill",
253            email: "marieriefake@email.com",
254            role: "Required",
255            status: "Accepted",
256          },
257          {
258            name: "Foua Khang",
259            email: "khanghangemail@email.com",
260            role: "Required",
261            status: "Accepted",
262          },
263          {
264            name: "Mindy Lee",
265            email: "mindyfakeemaillee@email.com",
266            role: "Required",
267            status: "Accepted",
268          },
269        ],
270        recurrenceRules: null,
271      },
272      {
273        id: "event_id_4", // will be overwritten after a call to calendarService.addEvent()
274        isAllDay: false,
275        startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(12, 30, 0), // 12:30 PM
276        endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(13, 30, 0), // 1:30 PM
277        availability: "Tentative",
278        status: "Tentative",
279        calendarId: null, // will be assigned to the default device calendar
280        title: "Lunch with Jennifer West",
281        location: "171 2nd St, San Francisco, CA 94105",
282        notes: "Bring latest version of contract",
283        alarms: [{ relativeOffsetSeconds: 1800 }], // 30 mins before event
284        attendees: [
285          {
286            name: "Awanasa Locklear",
287            email: "this_is_awanasa_fake@email.com",
288            role: "Required",
289            status: "Tentative",
290          },
291          {
292            name: "Elena Nieto",
293            email: "elenasfakeemail@email.com",
294            role: "Required",
295            status: "Tentative",
296          },
297        ],
298        recurrenceRules: null,
299      },
300      {
301        id: "event_id_5", // will be overwritten after a call to calendarService.addEvent()
302        isAllDay: false,
303        startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(14, 30, 0), // 2:30 PM
304        endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(15, 30, 0), // 3:30 PM
305        availability: "Tentative",
306        status: "Tentative",
307        calendarId: null, // will be assigned to the default device calendar
308        title: "Sales Workgroup",
309        location: "3270 Armbrester Drive, Gardena, CA 90248",
310        notes: "Discuss the new customer opportunities",
311        alarms: [{ relativeOffsetSeconds: 1800 }], // 30 mins before event
312        attendees: [
313          {
314            name: "Raul Nieto",
315            email: "raul.fake.nieto@email.com",
316            role: "Required",
317            status: "Accepted",
318          },
319          {
320            name: "Salome Ofodu",
321            email: "salomesalomefakeemail@email.com",
322            role: "Required",
323            status: "Tentative",
324          },
325          {
326            name: "Justus Pardo",
327            email: "justuspardofake@email.com",
328            role: "Required",
329            status: "Tentative",
330          },
331          {
332            name: "Gorav Patel",
333            email: "gpatel.fake.email@email.com",
334            role: "Required",
335            status: "Pending",
336          },
337        ],
338        recurrenceRules: null,
339      },
340      {
341        id: "event_id_6", // will be overwritten after a call to calendarService.addEvent()
342        isAllDay: false,
343        startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(15, 30, 0), // 3:30 PM
344        endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(17, 30, 0), // 5:30 PM
345        availability: "Busy",
346        status: "Confirmed",
347        calendarId: null, // will be assigned to the default device calendar
348        title: "Executive Team",
349        location: "56 Main Street, Seattle, WA 98119",
350        notes: "Report on Q2 sales and new leads",
351        alarms: [{ relativeOffsetSeconds: 600 }, { relativeOffsetSeconds: 3600 }], // 10 mins and 1 hour before event
352        attendees: [
353          {
354            name: "Florentina Perez",
355            email: "florperfakeemail@email.com",
356            role: "Required",
357            status: "Accepted",
358          },
359          {
360            name: "Harryette Randall",
361            email: "doubleletteremailfake@email.com",
362            role: "Required",
363            status: "Accepted",
364          },
365          {
366            name: "Sofia Rivera",
367            email: "fakeemailforsofie@email.com",
368            role: "Required",
369            status: "Accepted",
370          },
371        ],
372        recurrenceRules: null,
373      },
374    ];
375
376    return events;
377  }
378
379  getTodayTimestampAtTimeOfDay(hours, minutes, seconds) {
380    let d = new Date();
381    d.setHours(hours, minutes, seconds, 0);
382    return d.getTime() / 1000; // milliseconds to seconds
383  }
384
385  timeOfDayToString(dateSecondsUTC) {
386    let d = new Date(dateSecondsUTC * 1000); // seconds to milliseconds
387    let ampm = "AM";
388    let str = "";
389
390    if (d.getHours() > 12) {
391      ampm = "PM";
392      str += `${d.getHours() - 12}`;
393    } else {
394      str += `${d.getHours()}`;
395    }
396
397    if (d.getMinutes() > 0) {
398      if (d.getMinutes() < 10) {
399        str += `:0${d.getMinutes()}`;
400      } else {
401        str += `:${d.getMinutes()}`;
402      }
403    }
404
405    str += ` ${ampm}`;
406
407    return str;
408  }
409
410  alarmsToString(alarms) {
411    let results = [];
412
413    if (alarms) {
414      alarms.forEach((alarm) => {
415        if (alarm.relativeOffsetSeconds == 0) {
416          results.push("At time of event");
417        } else {
418          const mins = parseInt(Math.ceil(Math.abs(alarm.relativeOffsetSeconds) / 60.0));
419          if (mins == 1) {
420            results.push("1 minute before");
421          } else if (mins < 60) {
422            results.push(`${mins} minutes before`);
423          } else {
424            const hours = parseInt(Math.ceil(mins / 60.0));
425            if (hours == 1) {
426              results.push("1 hour before");
427            } else if (hours < 24) {
428              results.push(`${hours} hours before`);
429            } else {
430              const days = parseInt(Math.ceil(hours / 24.0));
431              if (days == 1) {
432                results.push("1 day before");
433              } else {
434                results.push(`${days} days before`);
435              }
436            }
437          }
438        }
439      });
440    } else {
441      results.push("None");
442    }
443
444    return results;
445  }
446
447  generateDisplayFields(calendarEvent) {
448    // these are used for display purpose only
449    calendarEvent.startTimeDisplay = this.timeOfDayToString(calendarEvent.startDateSecondsUTC);
450    calendarEvent.endTimeDisplay = this.timeOfDayToString(calendarEvent.endDateSecondsUTC);
451    calendarEvent.alarmsDisplay = this.alarmsToString(calendarEvent.alarms);
452  }
453
454  showSuccessAlert(title, message) {
455    LightningAlert.open({
456      message: message,
457      theme: "success",
458      label: title,
459    });
460  }
461
462  showFailureAlert(title, message) {
463    console.log(`calendarService.isAvailable(): ${this.calendarService.isAvailable()}`);
464    console.log(`selectedItemIndex: ${this.selectedItemIndex}`);
465    console.log(`selectedItem: ${this.selectedItem.id}`);
466
467    LightningAlert.open({
468      message: message,
469      theme: "error", // a red theme intended for error states
470      label: title,
471    });
472  }
473}

See Also