slashdev 1.0.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.
- package/CLAUDE.md +5 -4
- package/GETTING-STARTED.md +68 -0
- package/claude-skills/salesforce-development/SKILL.md +341 -0
- package/claude-skills/salesforce-development/references/apex-patterns.md +388 -0
- package/claude-skills/salesforce-development/references/lwc-patterns.md +374 -0
- package/claude-skills/salesforce-development/references/testing-patterns.md +438 -0
- package/claude-skills/secure-deploy/SKILL.md +219 -0
- package/hooks/pre-install-guard.sh +88 -0
- package/package.json +1 -1
- package/src/commands/setup-internals.js +84 -0
- package/src/commands/setup.js +8 -2
- package/src/commands/update.js +5 -1
|
@@ -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'` |
|