Note: This release is in preview. Features described here don’t become generally available until the latest general availability date that Salesforce announces for this release. Before then, and where features are noted as beta, pilot, or developer preview, we can’t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.
CalendarService Example
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 class="slds-var-p-horizontal_medium slds-var-p-vertical_x-small" key={item.id} onclick={showDetailView} data-id={item.id}>
7 <p class="slds-text-heading_small"><b>{item.title}</b></p>
8 <p class="slds-text-heading_small">{item.startTimeDisplay} — {item.endTimeDisplay}</p>
9 </div>
10 </template>
11 </lightning-card>
12 </template>
13
14 <!-- Detail View -->
15 <template if:true={detailViewIsOpen}>
16 <div class="slds-var-p-around_medium" style="background-color: white; border-radius: 4px;">
17 <table>
18 <tbody>
19 <tr>
20 <td class="slds-align-top" width="1">
21 <lightning-icon icon-name="utility:chevronleft" onclick={hideDetailView}></lightning-icon>
22 </td>
23 <td class="sldx-align-top slds-text-heading_medium slds-var-p-bottom_medium slds-align_absolute-center">Event Details</td>
24 <td class="slds-align-top" width="1">
25 <lightning-button-menu alternative-text="Show menu" variant="border-filled" menu-alignment="auto">
26 <lightning-menu-item prefix-icon-name="utility:add" value="Add" label="Add to Device Calendar" onclick={addCalendarEvent}></lightning-menu-item>
27 <lightning-menu-item prefix-icon-name="utility:edit" value="Update" label="Update in Device Calendar" onclick={updateCalendarEvent}></lightning-menu-item>
28 <lightning-menu-item prefix-icon-name="utility:delete" value="Delete" label="Remove from Device Calendar" onclick={deleteCalendarEvent}></lightning-menu-item>
29 </lightning-button-menu>
30 </td>
31 </tr>
32 <tr>
33 <td colspan="3">
34 <ul class="slds-has-dividers_bottom-space">
35 <li class="slds-item">
36 <span class="slds-text-heading_small"><b>{selectedItem.title}</b></span><br>
37 <span class="slds-text-heading_small">{selectedItem.startTimeDisplay} — {selectedItem.endTimeDisplay}</span>
38 </li>
39 <li class="slds-item">
40 <span class="slds-text-heading_small">Reminders</span><br>
41 <template for:each={selectedItem.alarmsDisplay} for:item="alarm">
42 <span class="slds-text-body_regular" key={alarm}>{alarm}<br></span>
43 </template>
44 </li>
45 <li class="slds-item">
46 <span class="slds-text-heading_small">Location</span><br>
47 <span class="slds-text-body_regular">{selectedItem.location}</span>
48 </li>
49 <li class="slds-item">
50 <span class="slds-text-heading_small">Attendees</span><br>
51 <template for:each={selectedItem.attendees} for:item="attendee">
52 <span class="slds-text-body_regular" key={attendee.name}>{attendee.name} ({attendee.email})<br></span>
53 </template>
54 </li>
55 <li class="slds-item">
56 <span class="slds-text-heading_small">Notes</span><br>
57 <span class="slds-text-body_regular">{selectedItem.notes}</span>
58 </li>
59 </ul>
60 </td>
61 </tr>
62 </tbody>
63 </table>
64 </div>
65 </template>
66 </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
8 todayEvents = [];
9 detailViewIsOpen = false;
10 selectedItem = null;
11 selectedItemIndex = -1;
12 calendarPermissionRationaleText = "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.addEvent(this.selectedItem, options)
55 .then((sanitizedEvent) => {
56 this.generateDisplayFields(sanitizedEvent);
57 this.selectedItem = sanitizedEvent;
58 this.todayEvents[this.selectedItemIndex] = sanitizedEvent;
59 this.showSuccessAlert("Add Event", "Event was added successfully to the device default calendar.");
60 console.log(`sanitizedEvent: ${JSON.stringify(sanitizedEvent)}`);
61 })
62 .catch((error) => {
63 console.error(error);
64 this.showFailureAlert("Add Event", `There was a problem adding the event to the device default calendar: ${error.message}`);
65 });
66 } else {
67 console.log("Calendar Service Is Not Available");
68 this.showFailureAlert("Add Event", "Calendar Service is not available.");
69 }
70 }
71
72 updateCalendarEvent() {
73 if (this.calendarService.isAvailable() && this.selectedItemIndex != -1 && this.selectedItem) {
74
75 // For this sample code, we've hard-coded some trivial changes
76
77 this.selectedItem.title += " - Updated";
78 this.selectedItem.notes += " - Updated";
79
80 const options = {
81 "permissionRationaleText" : this.calendarPermissionRationaleText,
82 "span" : "ThisEvent"
83 };
84
85 console.log(`options: ${JSON.stringify(options)}`);
86 console.log(`Updating selectedItem: ${JSON.stringify(this.selectedItem)}`);
87
88 this.calendarService.updateEvent(this.selectedItem, options)
89 .then((sanitizedEvent) => {
90 this.generateDisplayFields(sanitizedEvent);
91 this.selectedItem = sanitizedEvent;
92 this.todayEvents[this.selectedItemIndex] = sanitizedEvent;
93 this.showSuccessAlert("Update Event", "Event was updated successfully in the device default calendar.");
94 console.log(`sanitizedEvent: ${JSON.stringify(sanitizedEvent)}`);
95 })
96 .catch((error) => {
97 console.error(error);
98 this.showFailureAlert("Update Event", `There was a problem updating the event in the device default calendar: ${error.message}`);
99 });
100 } else {
101 console.log("Calendar Service Is Not Available");
102 this.showFailureAlert("Update Event", "Calendar Service is not available.");
103 }
104 }
105
106 deleteCalendarEvent() {
107 if (this.calendarService.isAvailable() && this.selectedItemIndex != -1 && this.selectedItem) {
108 LightningConfirm.open(
109 {
110 label: "Delete Event",
111 message: "Are you sure you want to delete this event?",
112 theme: "warning"
113 }
114 ).then((response) => {
115 if (response === true) {
116 const options = {
117 "permissionRationaleText" : this.calendarPermissionRationaleText,
118 "span" : "ThisEvent"
119 };
120
121 console.log(`options: ${JSON.stringify(options)}`);
122 console.log(`Deleting selectedItem: ${JSON.stringify(this.selectedItem)}`);
123
124 this.calendarService.removeEvent(this.selectedItem, options)
125 .then(() => {
126 this.todayEvents.splice(this.selectedItemIndex, 1);
127 this.hideDetailView();
128 this.showSuccessAlert("Delete Event", "Event was removed successfully from the device default calendar.");
129 })
130 .catch((error) => {
131 console.error(error);
132 this.showFailureAlert("Delete Event", `There was a problem removing the event from the device default calendar: ${error.message}`);
133 });
134 }
135 });
136 } else {
137 console.log("Calendar Service Is Not Available");
138 this.showFailureAlert("Delete Event", "Calendar Service is not available.");
139 }
140 }
141
142 getTodayEvents() {
143 // For this sample code, we've hard-coded some made-up values.
144 // Your component should fetch events from your SF org and convert them to the following object format.
145 const events = [
146 {
147 id: "event_id_1", // will be overwritten after a call to calendarService.addEvent()
148 isAllDay: false,
149 startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(8, 0, 0), // 8 AM
150 endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(10, 0, 0), // 10 AM
151 availability: "Busy",
152 status: "Confirmed",
153 calendarId: null, // will be assigned to the default device calendar
154 title: "Team Meeting",
155 location: "3514 Ruckman Road, San Francisco, CA 94105",
156 notes: "Discussing customer request for new calendar feature",
157 alarms: [{relativeOffsetSeconds: 600}], // 10 mins before event
158 attendees: [
159 { name: "Jamal Booker", email: "jbooker_fake_email@email.com", role: "Required", status: "Accepted" },
160 { name: "Robert Bullard", email: "bob.bullard.fake.email@email.com", role: "Required", status: "Pending" },
161 { name: "Gordon Chu", email: "gordonchu736251_email@email.com", role: "Optional", status: "Declined" },
162 ],
163 recurrenceRules: null
164 },
165 {
166 id: "event_id_2", // will be overwritten after a call to calendarService.addEvent()
167 isAllDay: false,
168 startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(10, 30, 0), // 10:30 AM
169 endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(11, 0, 0), // 11 AM
170 availability: "Busy",
171 status: "Confirmed",
172 calendarId: null, // will be assigned to the default device calendar
173 title: "Quarterly Review",
174 location: "2135 Alpha Avenue, Fernandina Beach, FL 32034",
175 notes: "Reviewing results of Q2 and planning Q3",
176 alarms: [{relativeOffsetSeconds: 1800}], // 30 mins before event
177 attendees: [
178 { name: "Alex Driskel", email: "adriskell_fake_email@email.com", role: "Required", status: "Accepted" },
179 { name: "Kim Friedman", email: "nothing_is_kimpossible_fake_email@email.com", role: "Required", status: "Tentative" },
180 { name: "April Guthman", email: "guthman.april.fake@email.com", role: "Required", status: "Pending" },
181 { name: "Leif Hansen", email: "leifhansenfakeemail@email.com", role: "Required", status: "Accepted" },
182 ],
183 recurrenceRules: null
184 },
185 {
186 id: "event_id_3", // will be overwritten after a call to calendarService.addEvent()
187 isAllDay: false,
188 startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(11, 0, 0), // 11 AM
189 endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(12, 0, 0), // 12 PM
190 availability: "Busy",
191 status: "Confirmed",
192 calendarId: null, // will be assigned to the default device calendar
193 title: "Portfolio Checklist",
194 location: "2281 Radford Street, Louisville, KY 40291",
195 notes: "Creating a guide to help sales in compiling a strong portfolio",
196 alarms: [{relativeOffsetSeconds: 600}], // 10 mins before event
197 attendees: [
198 { name: "Marie Hill", email: "marieriefake@email.com", role: "Required", status: "Accepted" },
199 { name: "Foua Khang", email: "khanghangemail@email.com", role: "Required", status: "Accepted" },
200 { name: "Mindy Lee", email: "mindyfakeemaillee@email.com", role: "Required", status: "Accepted" },
201 ],
202 recurrenceRules: null
203 },
204 {
205 id: "event_id_4", // will be overwritten after a call to calendarService.addEvent()
206 isAllDay: false,
207 startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(12, 30, 0), // 12:30 PM
208 endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(13, 30, 0), // 1:30 PM
209 availability: "Tentative",
210 status: "Tentative",
211 calendarId: null, // will be assigned to the default device calendar
212 title: "Lunch with Jennifer West",
213 location: "171 2nd St, San Francisco, CA 94105",
214 notes: "Bring latest version of contract",
215 alarms: [{relativeOffsetSeconds: 1800}], // 30 mins before event
216 attendees: [
217 { name: "Awanasa Locklear", email: "this_is_awanasa_fake@email.com", role: "Required", status: "Tentative" },
218 { name: "Elena Nieto", email: "elenasfakeemail@email.com", role: "Required", status: "Tentative" },
219 ],
220 recurrenceRules: null
221 },
222 {
223 id: "event_id_5", // will be overwritten after a call to calendarService.addEvent()
224 isAllDay: false,
225 startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(14, 30, 0), // 2:30 PM
226 endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(15, 30, 0), // 3:30 PM
227 availability: "Tentative",
228 status: "Tentative",
229 calendarId: null, // will be assigned to the default device calendar
230 title: "Sales Workgroup",
231 location: "3270 Armbrester Drive, Gardena, CA 90248",
232 notes: "Discuss the new customer opportunities",
233 alarms: [{relativeOffsetSeconds: 1800}], // 30 mins before event
234 attendees: [
235 { name: "Raul Nieto", email: "raul.fake.nieto@email.com", role: "Required", status: "Accepted" },
236 { name: "Salome Ofodu", email: "salomesalomefakeemail@email.com", role: "Required", status: "Tentative" },
237 { name: "Justus Pardo", email: "justuspardofake@email.com", role: "Required", status: "Tentative" },
238 { name: "Gorav Patel", email: "gpatel.fake.email@email.com", role: "Required", status: "Pending" },
239 ],
240 recurrenceRules: null
241 },
242 {
243 id: "event_id_6", // will be overwritten after a call to calendarService.addEvent()
244 isAllDay: false,
245 startDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(15, 30, 0), // 3:30 PM
246 endDateSecondsUTC: this.getTodayTimestampAtTimeOfDay(17, 30, 0), // 5:30 PM
247 availability: "Busy",
248 status: "Confirmed",
249 calendarId: null, // will be assigned to the default device calendar
250 title: "Executive Team",
251 location: "56 Main Street, Seattle, WA 98119",
252 notes: "Report on Q2 sales and new leads",
253 alarms: [{relativeOffsetSeconds: 600}, {relativeOffsetSeconds: 3600}], // 10 mins and 1 hour before event
254 attendees: [
255 { name: "Florentina Perez", email: "florperfakeemail@email.com", role: "Required", status: "Accepted" },
256 { name: "Harryette Randall", email: "doubleletteremailfake@email.com", role: "Required", status: "Accepted" },
257 { name: "Sofia Rivera", email: "fakeemailforsofie@email.com", role: "Required", status: "Accepted" },
258 ],
259 recurrenceRules: null
260 }
261 ];
262
263 return events;
264 }
265
266 getTodayTimestampAtTimeOfDay(hours, minutes, seconds) {
267 let d = new Date();
268 d.setHours(hours, minutes, seconds, 0);
269 return d.getTime() / 1000; // milliseconds to seconds
270 }
271
272 timeOfDayToString(dateSecondsUTC) {
273 let d = new Date(dateSecondsUTC * 1000); // seconds to milliseconds
274 let ampm = "AM";
275 let str = "";
276
277 if (d.getHours() > 12) {
278 ampm = "PM";
279 str += `${d.getHours() - 12}`;
280 } else {
281 str += `${d.getHours()}`;
282 }
283
284 if (d.getMinutes() > 0) {
285 if (d.getMinutes() < 10) {
286 str += `:0${d.getMinutes()}`;
287 } else {
288 str += `:${d.getMinutes()}`;
289 }
290 }
291
292 str += ` ${ampm}`;
293
294 return str;
295 }
296
297 alarmsToString(alarms) {
298 let results = [];
299
300 if (alarms) {
301 alarms.forEach(alarm => {
302 if (alarm.relativeOffsetSeconds == 0) {
303 results.push("At time of event");
304 } else {
305 const mins = parseInt(Math.ceil(Math.abs(alarm.relativeOffsetSeconds) / 60.0));
306 if (mins == 1) {
307 results.push("1 minute before");
308 } else if (mins < 60) {
309 results.push(`${mins} minutes before`);
310 } else {
311 const hours = parseInt(Math.ceil(mins / 60.0));
312 if (hours == 1) {
313 results.push("1 hour before");
314 } else if (hours < 24) {
315 results.push(`${hours} hours before`);
316 } else {
317 const days = parseInt(Math.ceil(hours / 24.0));
318 if (days == 1) {
319 results.push("1 day before");
320 } else {
321 results.push(`${days} days before`);
322 }
323 }
324 }
325 }
326 });
327 } else {
328 results.push("None");
329 }
330
331 return results;
332 }
333
334 generateDisplayFields(calendarEvent) {
335 // these are used for display purpose only
336 calendarEvent.startTimeDisplay = this.timeOfDayToString(calendarEvent.startDateSecondsUTC);
337 calendarEvent.endTimeDisplay = this.timeOfDayToString(calendarEvent.endDateSecondsUTC);
338 calendarEvent.alarmsDisplay = this.alarmsToString(calendarEvent.alarms);
339 }
340
341 showSuccessAlert(title, message) {
342 LightningAlert.open(
343 {
344 message: message,
345 theme: "success",
346 label: title,
347 }
348 );
349 }
350
351 showFailureAlert(title, message) {
352 console.log(`calendarService.isAvailable(): ${this.calendarService.isAvailable()}`);
353 console.log(`selectedItemIndex: ${this.selectedItemIndex}`);
354 console.log(`selectedItem: ${this.selectedItem.id}`);
355
356 LightningAlert.open(
357 {
358 message: message,
359 theme: "error", // a red theme intended for error states
360 label: title,
361 }
362 );
363 }
364}