slashdev 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,374 @@
1
+ # LWC Patterns — Reference
2
+
3
+ Load this file when building or modifying Lightning Web Components.
4
+
5
+ ---
6
+
7
+ ## Parent-Child Communication
8
+
9
+ ### Parent to child — public properties and methods
10
+
11
+ Expose properties with `@api` on the child. The parent sets them declaratively in the template.
12
+
13
+ ```javascript
14
+ // childComponent.js
15
+ import { LightningElement, api } from 'lwc';
16
+
17
+ export default class ChildComponent extends LightningElement {
18
+ @api recordId;
19
+ @api variant = 'default';
20
+
21
+ // Public method — parent can call this imperatively
22
+ @api
23
+ reset() {
24
+ this.template.querySelectorAll('lightning-input').forEach(input => input.value = '');
25
+ }
26
+ }
27
+ ```
28
+
29
+ ```html
30
+ <!-- parentComponent.html -->
31
+ <template>
32
+ <c-child-component record-id={selectedId} variant="compact"></c-child-component>
33
+ </template>
34
+ ```
35
+
36
+ Call a child's public method from the parent:
37
+
38
+ ```javascript
39
+ // parentComponent.js
40
+ handleReset() {
41
+ this.template.querySelector('c-child-component').reset();
42
+ }
43
+ ```
44
+
45
+ ### Child to parent — CustomEvent dispatch
46
+
47
+ Children communicate upward by dispatching `CustomEvent`. The parent listens with an
48
+ `on<eventname>` handler in the template. Event names must be lowercase, no underscores.
49
+
50
+ ```javascript
51
+ // childComponent.js
52
+ handleSelect(event) {
53
+ const selectedId = event.target.dataset.id;
54
+ this.dispatchEvent(new CustomEvent('itemselect', {
55
+ detail: { recordId: selectedId },
56
+ bubbles: false, // default — only the direct parent receives it
57
+ composed: false // default — does not cross shadow DOM boundary
58
+ }));
59
+ }
60
+ ```
61
+
62
+ ```html
63
+ <!-- parentComponent.html -->
64
+ <template>
65
+ <c-child-component onitemselect={handleItemSelect}></c-child-component>
66
+ </template>
67
+ ```
68
+
69
+ ```javascript
70
+ // parentComponent.js
71
+ handleItemSelect(event) {
72
+ this.selectedRecordId = event.detail.recordId;
73
+ }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Lightning Message Service (LMS)
79
+
80
+ Use LMS for communication between **unrelated** components — components in different DOM
81
+ trees, Aura and LWC, or even Visualforce pages on the same Lightning page.
82
+
83
+ ### 1. Create a message channel
84
+
85
+ ```xml
86
+ <!-- force-app/main/default/messageChannels/RecordSelected.messageChannel-meta.xml -->
87
+ <?xml version="1.0" encoding="UTF-8"?>
88
+ <LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
89
+ <masterLabel>Record Selected</masterLabel>
90
+ <isExposed>true</isExposed>
91
+ <lightningMessageFields>
92
+ <fieldName>recordId</fieldName>
93
+ </lightningMessageFields>
94
+ </LightningMessageChannel>
95
+ ```
96
+
97
+ ### 2. Publish
98
+
99
+ ```javascript
100
+ import { LightningElement, wire } from 'lwc';
101
+ import { publish, MessageContext } from 'lightning/messageService';
102
+ import RECORD_SELECTED from '@salesforce/messageChannel/RecordSelected__c';
103
+
104
+ export default class PublisherComponent extends LightningElement {
105
+ @wire(MessageContext) messageContext;
106
+
107
+ handleClick(event) {
108
+ publish(this.messageContext, RECORD_SELECTED, {
109
+ recordId: event.target.dataset.id
110
+ });
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### 3. Subscribe
116
+
117
+ ```javascript
118
+ import { LightningElement, wire } from 'lwc';
119
+ import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';
120
+ import RECORD_SELECTED from '@salesforce/messageChannel/RecordSelected__c';
121
+
122
+ export default class SubscriberComponent extends LightningElement {
123
+ @wire(MessageContext) messageContext;
124
+ subscription = null;
125
+ selectedRecordId;
126
+
127
+ connectedCallback() {
128
+ this.subscription = subscribe(this.messageContext, RECORD_SELECTED, (message) => {
129
+ this.selectedRecordId = message.recordId;
130
+ });
131
+ }
132
+
133
+ disconnectedCallback() {
134
+ unsubscribe(this.subscription);
135
+ this.subscription = null;
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## @wire with Reactive Parameters and refreshApex
143
+
144
+ ### Reactive parameters
145
+
146
+ Prefix a property with `$` in the `@wire` decorator to make the wire re-fire when the
147
+ property changes.
148
+
149
+ ```javascript
150
+ import { LightningElement, wire } from 'lwc';
151
+ import getContacts from '@salesforce/apex/ContactController.getContacts';
152
+
153
+ export default class ContactList extends LightningElement {
154
+ selectedAccountId;
155
+
156
+ @wire(getContacts, { accountId: '$selectedAccountId' })
157
+ contacts;
158
+
159
+ handleAccountChange(event) {
160
+ this.selectedAccountId = event.detail.value;
161
+ // Wire automatically re-fires — no manual call needed
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Refreshing cached wire data
167
+
168
+ After a write operation, the wire cache is stale. Use `refreshApex` to force a re-fetch.
169
+
170
+ ```javascript
171
+ import { refreshApex } from '@salesforce/apex';
172
+
173
+ // Store the full wire result (not just .data)
174
+ @wire(getContacts, { accountId: '$selectedAccountId' })
175
+ wiredContactsResult;
176
+
177
+ async handleSave() {
178
+ await saveContact({ contact: this.editedContact });
179
+ // Refresh the wire — pass the ENTIRE wire result, not .data
180
+ await refreshApex(this.wiredContactsResult);
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## uiRecordApi vs Apex
187
+
188
+ `lightning/uiRecordApi` provides client-side record CRUD without writing Apex. It respects
189
+ FLS and sharing automatically. Use it for simple single-record operations.
190
+
191
+ ### When to use uiRecordApi
192
+
193
+ - Reading/writing a **single record** with known fields
194
+ - You want **automatic FLS enforcement** without extra code
195
+ - No complex logic or cross-object queries needed
196
+
197
+ ### When to use Apex
198
+
199
+ - Querying **multiple records** with filters, joins, or aggregations
200
+ - Complex business logic before/after the DML
201
+ - Bulk operations
202
+ - Callouts or async processing
203
+
204
+ ### Examples
205
+
206
+ ```javascript
207
+ import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
208
+ import { updateRecord } from 'lightning/uiRecordApi';
209
+ import { createRecord } from 'lightning/uiRecordApi';
210
+ import { deleteRecord } from 'lightning/uiRecordApi';
211
+ import NAME_FIELD from '@salesforce/schema/Account.Name';
212
+ import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
213
+
214
+ // Read
215
+ @wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, INDUSTRY_FIELD] })
216
+ account;
217
+
218
+ get accountName() {
219
+ return getFieldValue(this.account.data, NAME_FIELD);
220
+ }
221
+
222
+ // Update
223
+ async handleUpdate() {
224
+ const fields = {};
225
+ fields.Id = this.recordId;
226
+ fields[INDUSTRY_FIELD.fieldApiName] = 'Technology';
227
+ await updateRecord({ fields });
228
+ }
229
+
230
+ // Create
231
+ async handleCreate() {
232
+ const fields = {};
233
+ fields[NAME_FIELD.fieldApiName] = 'New Account';
234
+ const record = await createRecord({ apiName: 'Account', fields });
235
+ this.newRecordId = record.id;
236
+ }
237
+
238
+ // Delete
239
+ async handleDelete() {
240
+ await deleteRecord(this.recordId);
241
+ }
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Error Handling — reduceErrors and ShowToastEvent
247
+
248
+ ### reduceErrors utility
249
+
250
+ LWC errors come in multiple shapes (wire errors, imperative errors, UI API errors). This
251
+ utility normalizes them into a string array.
252
+
253
+ ```javascript
254
+ // utils/reduceErrors.js
255
+ export function reduceErrors(errors) {
256
+ if (!Array.isArray(errors)) {
257
+ errors = [errors];
258
+ }
259
+
260
+ return errors
261
+ .filter((error) => !!error)
262
+ .map((error) => {
263
+ if (Array.isArray(error.body)) {
264
+ return error.body.map((e) => e.message);
265
+ }
266
+ if (typeof error.body?.message === 'string') {
267
+ return error.body.message;
268
+ }
269
+ if (typeof error.message === 'string') {
270
+ return error.message;
271
+ }
272
+ return error.statusText || 'Unknown error';
273
+ })
274
+ .flat();
275
+ }
276
+ ```
277
+
278
+ ### ShowToastEvent for user-facing messages
279
+
280
+ ```javascript
281
+ import { ShowToastEvent } from 'lightning/platformShowToastEvent';
282
+
283
+ // Success
284
+ this.dispatchEvent(new ShowToastEvent({
285
+ title: 'Success',
286
+ message: 'Record saved successfully',
287
+ variant: 'success'
288
+ }));
289
+
290
+ // Error with reduceErrors
291
+ this.dispatchEvent(new ShowToastEvent({
292
+ title: 'Error',
293
+ message: reduceErrors(error).join(', '),
294
+ variant: 'error',
295
+ mode: 'sticky' // stays until dismissed — use for errors
296
+ }));
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Navigation via NavigationMixin
302
+
303
+ ```javascript
304
+ import { LightningElement } from 'lwc';
305
+ import { NavigationMixin } from 'lightning/navigation';
306
+
307
+ export default class NavigationExample extends NavigationMixin(LightningElement) {
308
+
309
+ // Navigate to a record page
310
+ navigateToRecord(recordId) {
311
+ this[NavigationMixin.Navigate]({
312
+ type: 'standard__recordPage',
313
+ attributes: {
314
+ recordId: recordId,
315
+ objectApiName: 'Account',
316
+ actionName: 'view'
317
+ }
318
+ });
319
+ }
320
+
321
+ // Navigate to a list view
322
+ navigateToListView() {
323
+ this[NavigationMixin.Navigate]({
324
+ type: 'standard__objectPage',
325
+ attributes: {
326
+ objectApiName: 'Account',
327
+ actionName: 'list'
328
+ },
329
+ state: {
330
+ filterName: 'Recent'
331
+ }
332
+ });
333
+ }
334
+
335
+ // Navigate to a custom tab / Lightning page
336
+ navigateToTab() {
337
+ this[NavigationMixin.Navigate]({
338
+ type: 'standard__navItemPage',
339
+ attributes: {
340
+ apiName: 'MyCustomTab'
341
+ }
342
+ });
343
+ }
344
+
345
+ // Generate a URL without navigating
346
+ async getRecordUrl(recordId) {
347
+ return this[NavigationMixin.GenerateUrl]({
348
+ type: 'standard__recordPage',
349
+ attributes: {
350
+ recordId: recordId,
351
+ actionName: 'view'
352
+ }
353
+ });
354
+ }
355
+ }
356
+ ```
357
+
358
+ ---
359
+
360
+ ## @salesforce/* Scoped Imports
361
+
362
+ These are compile-time imports resolved by the Lightning platform. They cannot be mocked
363
+ in standard Jest tests without `@salesforce/sfdx-lwc-jest` shims.
364
+
365
+ | Import | Purpose | Example |
366
+ |--------|---------|---------|
367
+ | `@salesforce/apex/ClassName.methodName` | Call an Apex method | `import getAccounts from '@salesforce/apex/AccountCtrl.getAccounts'` |
368
+ | `@salesforce/schema/Object.Field` | Reference a field (compile-time validated) | `import NAME from '@salesforce/schema/Account.Name'` |
369
+ | `@salesforce/user/Id` | Current user's ID | `import userId from '@salesforce/user/Id'` |
370
+ | `@salesforce/label/c.MyLabel` | Custom label | `import greeting from '@salesforce/label/c.WelcomeMessage'` |
371
+ | `@salesforce/resourceUrl/MyResource` | Static resource URL | `import LOGO from '@salesforce/resourceUrl/CompanyLogo'` |
372
+ | `@salesforce/i18n/lang` | User's language/locale | `import LANG from '@salesforce/i18n/lang'` |
373
+ | `@salesforce/messageChannel/ChannelName__c` | Message channel for LMS | See LMS section above |
374
+ | `@salesforce/customPermission/PermName` | Check custom permission | `import hasPerm from '@salesforce/customPermission/ManageAccounts'` |