fe-lwc-skill 1.0.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,227 @@
1
+ # events-custom-event
2
+
3
+ **Impact: HIGH**
4
+
5
+ Custom events are the standard mechanism for child-to-parent communication in LWC. Two boolean flags — `bubbles` and `composed` — control how far an event travels through the DOM.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Always declare `bubbles` and `composed` explicitly.
10
+ > - Prefer **kebab-case** event names (`step-completed`) — camelCase is valid but less portable across some platforms.
11
+ > - The template handler attribute is always `on` + the full event name: `onstep-completed` or `onstepcompleted`.
12
+ > - Pass data through `detail`, not as top-level properties on the event object.
13
+ > - Keep `composed: true` only when the listener lives outside the component's shadow root.
14
+
15
+ ---
16
+
17
+ ## Decision Guide: `bubbles` and `composed`
18
+
19
+ | Listener location | `bubbles` | `composed` |
20
+ | ---------------------------------------------------------- | ------------------- | ---------- |
21
+ | Direct parent only | `false` | `false` |
22
+ | Any ancestor in the same shadow tree | `true` | `false` |
23
+ | Outside the shadow root (cross-shadow, App Builder region) | `true` | `true` |
24
+ | Cross-region on Experience Cloud | Use **LMS** instead | — |
25
+
26
+ ---
27
+
28
+ ## Case 1: Missing flags, data not in `detail`
29
+
30
+ **Incorrect:**
31
+
32
+ ```typescript
33
+ // appointmentCard.ts
34
+ import { LightningElement, api } from "lwc";
35
+
36
+ // @ts-ignore
37
+ export default class AppointmentCard extends LightningElement {
38
+ // @ts-ignore
39
+ @api recordId: string;
40
+
41
+ public handleCancel(): void {
42
+ // ❌ No bubbles/composed, data not in detail
43
+ const event = new CustomEvent("appointmentCancelled", {
44
+ appointmentId: this.recordId,
45
+ reason: "User cancelled",
46
+ } as CustomEventInit);
47
+ this.dispatchEvent(event);
48
+ }
49
+ }
50
+ ```
51
+
52
+ **Correct — explicit flags, data in `detail`:**
53
+
54
+ ```typescript
55
+ // appointmentCard.ts
56
+ import { LightningElement, api } from "lwc";
57
+
58
+ interface AppointmentCancelledDetail {
59
+ appointmentId: string;
60
+ reason: string;
61
+ }
62
+
63
+ // @ts-ignore
64
+ export default class AppointmentCard extends LightningElement {
65
+ // @ts-ignore
66
+ @api recordId: string;
67
+
68
+ public handleCancel(): void {
69
+ this.dispatchEvent(
70
+ new CustomEvent<AppointmentCancelledDetail>("appointmentCancelled", {
71
+ bubbles: false, // ✅ explicit — direct parent is the listener
72
+ composed: false, // ✅ explicit — same shadow root
73
+ detail: {
74
+ appointmentId: this.recordId,
75
+ reason: "User cancelled",
76
+ },
77
+ }),
78
+ );
79
+ }
80
+ }
81
+ ```
82
+
83
+ ```typescript
84
+ // parentContainer.ts
85
+ import { LightningElement } from "lwc";
86
+
87
+ interface AppointmentCancelledDetail {
88
+ appointmentId: string;
89
+ reason: string;
90
+ }
91
+
92
+ // @ts-ignore
93
+ export default class ParentContainer extends LightningElement {
94
+ public handleCancel(event: CustomEvent<AppointmentCancelledDetail>): void {
95
+ const { appointmentId, reason } = event.detail;
96
+ // handle cancellation...
97
+ }
98
+ }
99
+ ```
100
+
101
+ ```html
102
+ <!-- parentContainer.html -->
103
+ <c-appointment-card onappointmentcancelled="{handleCancel}"></c-appointment-card>
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Case 2: Event must bubble through multiple ancestors
109
+
110
+ Scenario: `appointmentStep` → `appointmentWizard` → `appointmentModal`. The modal is the listener.
111
+
112
+ **Incorrect — `bubbles: false` stops the event at the direct parent:**
113
+
114
+ ```typescript
115
+ // appointmentStep.ts
116
+ handleNext(): void {
117
+ this.dispatchEvent(
118
+ new CustomEvent('step-completed', {
119
+ bubbles: false, // ❌ stops at appointmentWizard
120
+ composed: false,
121
+ detail: { stepIndex: this.currentStep },
122
+ })
123
+ );
124
+ }
125
+ ```
126
+
127
+ **Correct:**
128
+
129
+ ```typescript
130
+ // appointmentStep.ts
131
+ import { LightningElement } from "lwc";
132
+
133
+ interface StepCompletedDetail {
134
+ stepIndex: number;
135
+ }
136
+
137
+ // @ts-ignore
138
+ export default class AppointmentStep extends LightningElement {
139
+ currentStep: number = 0;
140
+
141
+ public handleNext(): void {
142
+ this.dispatchEvent(
143
+ new CustomEvent<StepCompletedDetail>("step-completed", {
144
+ bubbles: true, // ✅ travels up to appointmentModal
145
+ composed: false, // ✅ still within the same shadow root
146
+ detail: { stepIndex: this.currentStep },
147
+ }),
148
+ );
149
+ }
150
+ }
151
+ ```
152
+
153
+ ```typescript
154
+ // appointmentModal.ts
155
+ import { LightningElement } from "lwc";
156
+
157
+ // @ts-ignore
158
+ export default class AppointmentModal extends LightningElement {
159
+ currentStep: number = 0;
160
+
161
+ public handleStepCompleted(event: CustomEvent<{ stepIndex: number }>): void {
162
+ this.currentStep = event.detail.stepIndex + 1;
163
+ }
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Case 3: `composed: true` when crossing shadow roots (slotted content)
170
+
171
+ **Incorrect:**
172
+
173
+ ```typescript
174
+ // slottedForm.ts
175
+ handleSubmit(): void {
176
+ this.dispatchEvent(
177
+ new CustomEvent('form-submitted', {
178
+ bubbles: true,
179
+ composed: false, // ❌ cannot cross the shadow root boundary
180
+ detail: { formData: this.formState },
181
+ })
182
+ );
183
+ }
184
+ ```
185
+
186
+ **Correct:**
187
+
188
+ ```typescript
189
+ // slottedForm.ts
190
+ import { LightningElement } from "lwc";
191
+
192
+ interface FormSubmittedDetail {
193
+ formData: Record<string, string>;
194
+ }
195
+
196
+ // @ts-ignore
197
+ export default class SlottedForm extends LightningElement {
198
+ formState: Record<string, string> = {};
199
+
200
+ public handleSubmit(): void {
201
+ this.dispatchEvent(
202
+ new CustomEvent<FormSubmittedDetail>("form-submitted", {
203
+ bubbles: true,
204
+ composed: true, // ✅ crosses the shadow boundary to the slot host
205
+ detail: { formData: this.formState },
206
+ }),
207
+ );
208
+ }
209
+ }
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Quick Decision Guide
215
+
216
+ | Situation | Pattern |
217
+ | -------------------------------------------------- | -------------------------------------------------------------- |
218
+ | Child notifies direct parent | `bubbles: false`, `composed: false` |
219
+ | Child notifies distant ancestor (same shadow tree) | `bubbles: true`, `composed: false` |
220
+ | Slotted component notifies slot host | `bubbles: true`, `composed: true` |
221
+ | Cross-region on App Builder / Experience Cloud | Use **LMS** |
222
+ | Event name | Prefer kebab-case; handler is `on` + name, all lowercase |
223
+ | Payload | Always in **`detail`** — typed with a generic `CustomEvent<T>` |
224
+
225
+ ---
226
+
227
+ **Reference:** [Create and Dispatch Events — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/events-create-dispatch.html)
@@ -0,0 +1,193 @@
1
+ # events-lms
2
+
3
+ **Impact: HIGH**
4
+
5
+ Lightning Message Service (LMS) is the platform-native solution for communication between components that have no parent-child relationship, or that live in different shadow roots across App Builder regions and Experience Cloud portal slots.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Use LMS when components have **no shared parent**, or live in **different App Builder regions / Experience Cloud slots**.
10
+ > - Always **unsubscribe** in `disconnectedCallback` to prevent memory leaks.
11
+ > - Define each message channel in a dedicated `.messageChannel` metadata file.
12
+
13
+ > **Note for Jest tests:** `@salesforce/messageChannel` requires a mock in Jest. Add the mock in `jest.config.js`:
14
+ >
15
+ > ```javascript
16
+ > moduleNameMapper: {
17
+ > '@salesforce/messageChannel/(.+)': '<rootDir>/path/to/__mocks__/messageChannel.js'
18
+ > }
19
+ > ```
20
+
21
+ ---
22
+
23
+ ## Decision Guide: LMS vs Custom Event
24
+
25
+ | Scenario | Use |
26
+ | --------------------------------------------------------- | -------------------------------------------------------- |
27
+ | Child notifies direct parent | Custom event (`bubbles: false`) |
28
+ | Child notifies distant ancestor (same shadow tree) | Custom event (`bubbles: true`) |
29
+ | Component in Experience Cloud header notifies main region | **LMS** |
30
+ | Two sibling components with no shared parent | **LMS** |
31
+ | Cross-tab or cross-page communication | Not supported — use server-side state or Platform Events |
32
+
33
+ ---
34
+
35
+ ## Step 1: Define the MessageChannel metadata file
36
+
37
+ ```xml
38
+ <!-- force-app/main/default/messageChannels/AppointmentSelected.messageChannel-meta.xml -->
39
+ <?xml version="1.0" encoding="UTF-8"?>
40
+ <LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
41
+ <masterLabel>AppointmentSelected</masterLabel>
42
+ <isExposed>true</isExposed>
43
+ <description>Notifies subscribers when a patient appointment is selected.</description>
44
+ <lightningMessageFields>
45
+ <fieldName>appointmentId</fieldName>
46
+ <description>Salesforce record Id of the selected appointment.</description>
47
+ </lightningMessageFields>
48
+ <lightningMessageFields>
49
+ <fieldName>patientName</fieldName>
50
+ <description>Display name of the patient.</description>
51
+ </lightningMessageFields>
52
+ </LightningMessageChannel>
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Step 2: Publisher
58
+
59
+ **Incorrect — custom event cannot reach a component in a different region:**
60
+
61
+ ```typescript
62
+ // appointmentList.ts
63
+ handleAppointmentClick(event: Event): void {
64
+ const target = event.currentTarget as HTMLElement;
65
+ this.dispatchEvent(
66
+ new CustomEvent('appointmentselected', {
67
+ bubbles: true,
68
+ composed: true, // ❌ still won't reach a separate region/slot
69
+ detail: { appointmentId: target.dataset.id },
70
+ })
71
+ );
72
+ }
73
+ ```
74
+
75
+ **Correct — publish via LMS:**
76
+
77
+ ```typescript
78
+ // appointmentList.ts
79
+ import { LightningElement, wire } from "lwc";
80
+ import { MessageContext, publish, MessageContextType } from "lightning/messageService";
81
+ import APPOINTMENT_SELECTED from "@salesforce/messageChannel/AppointmentSelected__c";
82
+
83
+ interface AppointmentSelectedPayload {
84
+ appointmentId: string;
85
+ patientName: string;
86
+ }
87
+
88
+ // @ts-ignore
89
+ export default class AppointmentList extends LightningElement {
90
+ // @ts-ignore
91
+ @wire(MessageContext)
92
+ messageContext: MessageContextType;
93
+
94
+ public handleAppointmentClick(event: Event): void {
95
+ const target = event.currentTarget as HTMLElement;
96
+
97
+ const payload: AppointmentSelectedPayload = {
98
+ appointmentId: target.dataset.id ?? "",
99
+ patientName: target.dataset.patientName ?? "",
100
+ };
101
+
102
+ publish(this.messageContext, APPOINTMENT_SELECTED, payload);
103
+ }
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Step 3: Subscriber
110
+
111
+ **Incorrect — subscribing without unsubscribing:**
112
+
113
+ ```typescript
114
+ // appointmentDetail.ts
115
+ import { LightningElement, wire } from "lwc";
116
+ import { MessageContext, subscribe, MessageContextType } from "lightning/messageService";
117
+ import APPOINTMENT_SELECTED from "@salesforce/messageChannel/AppointmentSelected__c";
118
+
119
+ // @ts-ignore
120
+ export default class AppointmentDetail extends LightningElement {
121
+ // @ts-ignore
122
+ @wire(MessageContext)
123
+ messageContext: MessageContextType;
124
+
125
+ public connectedCallback(): void {
126
+ subscribe(this.messageContext, APPOINTMENT_SELECTED, (message) => {
127
+ this.appointmentId = (message as { appointmentId: string }).appointmentId;
128
+ });
129
+ // ❌ return value discarded — cannot unsubscribe later
130
+ }
131
+ }
132
+ ```
133
+
134
+ **Correct — store the subscription handle and unsubscribe in `disconnectedCallback`:**
135
+
136
+ ```typescript
137
+ // appointmentDetail.ts
138
+ import { LightningElement, wire } from "lwc";
139
+ import {
140
+ MessageContext,
141
+ subscribe,
142
+ unsubscribe,
143
+ MessageContextType,
144
+ MessageServiceSubscription,
145
+ } from "lightning/messageService";
146
+ import APPOINTMENT_SELECTED from "@salesforce/messageChannel/AppointmentSelected__c";
147
+
148
+ interface AppointmentSelectedPayload {
149
+ appointmentId: string;
150
+ patientName: string;
151
+ }
152
+
153
+ // @ts-ignore
154
+ export default class AppointmentDetail extends LightningElement {
155
+ // @ts-ignore
156
+ @wire(MessageContext)
157
+ messageContext: MessageContextType;
158
+
159
+ subscription: MessageServiceSubscription | null = null;
160
+ appointmentId: string | null = null;
161
+
162
+ public connectedCallback(): void {
163
+ this.subscription = subscribe(this.messageContext, APPOINTMENT_SELECTED, (message) =>
164
+ this.handleMessage(message as AppointmentSelectedPayload),
165
+ );
166
+ }
167
+
168
+ public disconnectedCallback(): void {
169
+ unsubscribe(this.subscription); // ✅ clean up when component leaves the DOM
170
+ this.subscription = null;
171
+ }
172
+
173
+ private handleMessage(message: AppointmentSelectedPayload): void {
174
+ this.appointmentId = message.appointmentId;
175
+ }
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Quick Decision Guide
182
+
183
+ | Situation | Pattern |
184
+ | ---------------------------------------- | ----------------------------------------------------- |
185
+ | Same shadow tree, parent-child | Custom event |
186
+ | Different regions / unrelated components | LMS |
187
+ | Payload type | Typed interface + cast on receive |
188
+ | Unsubscribe | Always in `disconnectedCallback` |
189
+ | Jest tests | Mock `@salesforce/messageChannel` in `jest.config.js` |
190
+
191
+ ---
192
+
193
+ **Reference:** [Lightning Message Service — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/use-message-service.html)
@@ -0,0 +1,191 @@
1
+ # template-directives
2
+
3
+ **Impact: HIGH**
4
+
5
+ LWC introduced modern template directives in API version 58.0. The old `if:true` / `if:false` directives are deprecated. Always use `lwc:if` / `lwc:elseif` / `lwc:else` for conditionals and `for:each` with a `key` for loops.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Never use `if:true` or `if:false` — use `lwc:if` / `lwc:elseif` / `lwc:else`.
10
+ > - `lwc:elseif` and `lwc:else` must be **direct siblings** of `lwc:if` — no elements between them.
11
+ > - Always use `for:each` with a unique `key` on the **direct child element** — never on the `<template>` tag.
12
+ > - Never use array index as `key` — use a stable unique ID from the data.
13
+
14
+ ---
15
+
16
+ ## Case 1: Conditional rendering
17
+
18
+ **Incorrect — deprecated directives:**
19
+
20
+ ```html
21
+ <template>
22
+ <template if:true="{isLoading}">
23
+ <lightning-spinner></lightning-spinner>
24
+ </template>
25
+ <template if:false="{isLoading}">
26
+ <template if:true="{hasData}">
27
+ <p>{appointment.Name}</p>
28
+ </template>
29
+ </template>
30
+ </template>
31
+ ```
32
+
33
+ **Correct — modern directives:**
34
+
35
+ ```html
36
+ <template>
37
+ <template lwc:if="{isLoading}">
38
+ <lightning-spinner></lightning-spinner>
39
+ </template>
40
+
41
+ <template lwc:elseif="{hasData}">
42
+ <p>{appointment.Name}</p>
43
+ </template>
44
+
45
+ <template lwc:else>
46
+ <p>No appointment found.</p>
47
+ </template>
48
+ </template>
49
+ ```
50
+
51
+ > ⚠️ **Sibling rule:** `lwc:elseif` / `lwc:else` must immediately follow `lwc:if` with no elements in between, or the chain breaks silently.
52
+ >
53
+ > ```html
54
+ > <!-- ❌ Breaks the chain — p element between if and elseif -->
55
+ > <template lwc:if="{condA}">...</template>
56
+ > <p>something</p>
57
+ > <template lwc:elseif="{condB}">...</template>
58
+ > ```
59
+
60
+ ```typescript
61
+ // appointmentCard.ts
62
+ import { LightningElement, api, wire } from "lwc";
63
+ import { getRecord, getFieldValue } from "lightning/uiRecordApi";
64
+ import NAME_FIELD from "@salesforce/schema/Appointment__c.Name";
65
+
66
+ // @ts-ignore
67
+ export default class AppointmentCard extends LightningElement {
68
+ // @ts-ignore
69
+ @api recordId: string;
70
+
71
+ isLoading: boolean = true;
72
+ appointment: Record<string, unknown> | null = null;
73
+
74
+ // @ts-ignore
75
+ @wire(getRecord, { recordId: "$recordId", fields: [NAME_FIELD] })
76
+ public handleRecord({ data, error }: { data?: Record<string, unknown>; error?: unknown }): void {
77
+ this.isLoading = false;
78
+ if (data) {
79
+ this.appointment = data;
80
+ }
81
+ }
82
+
83
+ public get hasData(): boolean {
84
+ return !!this.appointment;
85
+ }
86
+
87
+ public get appointmentName(): string | null {
88
+ return getFieldValue(this.appointment, NAME_FIELD) as string | null;
89
+ }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Case 2: Rendering a list with `for:each`
96
+
97
+ **Incorrect — `key` on `<template>` tag (ignored by LWC engine):**
98
+
99
+ ```html
100
+ <!-- ❌ key on <template> is ignored -->
101
+ <template for:each="{appointments}" for:item="appt" key="{appt.Id}">
102
+ <div>
103
+ <p>{appt.Name}</p>
104
+ </div>
105
+ </template>
106
+ ```
107
+
108
+ **Incorrect — array index as key:**
109
+
110
+ ```html
111
+ <!-- ❌ causes incorrect DOM reuse on reorder/remove -->
112
+ <template for:each="{appointments}" for:item="appt" for:index="idx">
113
+ <div key="{idx}">
114
+ <p>{appt.Name}</p>
115
+ </div>
116
+ </template>
117
+ ```
118
+
119
+ **Correct — `key` on the direct child element, using a stable record ID:**
120
+
121
+ ```html
122
+ <template>
123
+ <template for:each="{appointments}" for:item="appt">
124
+ <div key="{appt.Id}">
125
+ <p>{appt.Name}</p>
126
+ <p>{appt.Status__c}</p>
127
+ </div>
128
+ </template>
129
+ </template>
130
+ ```
131
+
132
+ ```typescript
133
+ // appointmentList.ts
134
+ import { LightningElement, api, wire } from "lwc";
135
+ import getAppointments from "@salesforce/apex/AppointmentController.getAppointments";
136
+
137
+ interface AppointmentRecord {
138
+ Id: string;
139
+ Name: string;
140
+ Status__c: string;
141
+ IsUrgent__c: boolean;
142
+ }
143
+
144
+ // @ts-ignore
145
+ export default class AppointmentList extends LightningElement {
146
+ // @ts-ignore
147
+ @api recordId: string;
148
+
149
+ // @ts-ignore
150
+ @wire(getAppointments, { recordId: "$recordId" })
151
+ appointments: { data?: AppointmentRecord[]; error?: unknown };
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Case 3: Conditional rendering inside a loop
158
+
159
+ ```html
160
+ <template>
161
+ <template for:each="{appointments.data}" for:item="appt">
162
+ <div key="{appt.Id}" class="slds-m-bottom_small">
163
+ <p>{appt.Name}</p>
164
+
165
+ <template lwc:if="{appt.IsUrgent__c}">
166
+ <lightning-badge label="Urgent" class="slds-theme_error"></lightning-badge>
167
+ </template>
168
+
169
+ <template lwc:else>
170
+ <lightning-badge label="Routine"></lightning-badge>
171
+ </template>
172
+ </div>
173
+ </template>
174
+ </template>
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Quick Decision Guide
180
+
181
+ | Situation | Pattern |
182
+ | ------------------------------- | ----------------------------------------------------------- |
183
+ | Show/hide one block | `lwc:if` |
184
+ | if / else if / else | `lwc:if` + `lwc:elseif` + `lwc:else` (direct siblings only) |
185
+ | Render a list | `for:each` + `key` on direct child element |
186
+ | Key value | Stable record ID — never array index |
187
+ | Conditional content inside loop | `lwc:if` inside `for:each` |
188
+
189
+ ---
190
+
191
+ **Reference:** [Render Lists — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/create-components-directives.html)