CalendarService User Experience
Use the CalendarService API
CalendarService Example
Compatibility and Requirements
Considerations and Limitations
Previous Versions
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