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.
- package/README.md +56 -0
- package/bin/index.js +145 -0
- package/package.json +23 -0
- package/skill/SKILL.md +88 -0
- package/skill/rules/data-error-handling.md +297 -0
- package/skill/rules/data-lds-first.md +261 -0
- package/skill/rules/data-wire-vs-imperative.md +248 -0
- package/skill/rules/design-token-dxp.md +230 -0
- package/skill/rules/design-token-slds.md +258 -0
- package/skill/rules/events-custom-event.md +227 -0
- package/skill/rules/events-lms.md +193 -0
- package/skill/rules/template-directives.md +191 -0
- package/skill/rules/template-dom-querying.md +202 -0
- package/skill/rules/template-form-pattern.md +285 -0
- package/skill/rules/template-track-immutable.md +206 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# fe-lwc-skill
|
|
2
|
+
|
|
3
|
+
Salesforce LWC Frontend Skill for AI Agents — installs structured rule files for Lightning Web Components development into your project's agent config directory.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Auto-detect agent runtime (Claude Code, Cursor, Gemini CLI, Continue.dev, Copilot)
|
|
9
|
+
npx fe-lwc-skill
|
|
10
|
+
|
|
11
|
+
# Install to a specific path
|
|
12
|
+
npx fe-lwc-skill --target .claude/skills/fe-lwc
|
|
13
|
+
|
|
14
|
+
# Overwrite existing install
|
|
15
|
+
npx fe-lwc-skill --force
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Supported runtimes
|
|
19
|
+
|
|
20
|
+
| Runtime | Auto-detected via | Installs to |
|
|
21
|
+
| -------------- | ----------------- | ------------------------------ |
|
|
22
|
+
| Claude Code | `.claude/` | `.claude/skills/fe-lwc/` |
|
|
23
|
+
| Gemini CLI | `.gemini/` | `.gemini/skills/fe-lwc/` |
|
|
24
|
+
| Cursor | `.cursor/` | `.cursor/rules/fe-lwc/` |
|
|
25
|
+
| Continue.dev | `.continue/` | `.continue/rules/fe-lwc/` |
|
|
26
|
+
| GitHub Copilot | `.github/` | `.github/instructions/fe-lwc/` |
|
|
27
|
+
|
|
28
|
+
If multiple runtimes are detected, the skill is installed to all of them.
|
|
29
|
+
|
|
30
|
+
## What's included
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
skill/
|
|
34
|
+
├── SKILL.md # Agent entry point
|
|
35
|
+
└── rules/
|
|
36
|
+
├── data-wire-vs-imperative.md
|
|
37
|
+
├── data-lds-first.md
|
|
38
|
+
├── data-error-handling.md
|
|
39
|
+
├── template-directives.md
|
|
40
|
+
├── template-form-pattern.md
|
|
41
|
+
├── template-dom-query.md
|
|
42
|
+
├── events-custom-event.md
|
|
43
|
+
├── events-lms.md
|
|
44
|
+
├── design-token-slds.md
|
|
45
|
+
└── design-token-dxp.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## After install
|
|
49
|
+
|
|
50
|
+
Reference the skill in your `CLAUDE.md` / `AGENTS.md`:
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
## Skills
|
|
54
|
+
|
|
55
|
+
- LWC Frontend: `.claude/skills/fe-lwc/SKILL.md`
|
|
56
|
+
```
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cpSync, mkdirSync, existsSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const SKILL_NAME = "fe-lwc";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Known agent runtimes and where they expect skill/rule files.
|
|
11
|
+
* Add new entries here as new agents emerge.
|
|
12
|
+
*/
|
|
13
|
+
const AGENT_RUNTIMES = [
|
|
14
|
+
{
|
|
15
|
+
id: "claude-code",
|
|
16
|
+
label: "Claude Code",
|
|
17
|
+
markerDir: ".claude",
|
|
18
|
+
skillPath: `.claude/skills/${SKILL_NAME}`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "gemini",
|
|
22
|
+
label: "Gemini CLI",
|
|
23
|
+
markerDir: ".gemini",
|
|
24
|
+
skillPath: `.gemini/skills/${SKILL_NAME}`,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "cursor",
|
|
28
|
+
label: "Cursor",
|
|
29
|
+
markerDir: ".cursor",
|
|
30
|
+
skillPath: `.cursor/rules/${SKILL_NAME}`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "continue",
|
|
34
|
+
label: "Continue.dev",
|
|
35
|
+
markerDir: ".continue",
|
|
36
|
+
skillPath: `.continue/rules/${SKILL_NAME}`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "copilot",
|
|
40
|
+
label: "GitHub Copilot",
|
|
41
|
+
markerDir: ".github",
|
|
42
|
+
skillPath: `.github/instructions/${SKILL_NAME}`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
49
|
+
const skillSrc = join(__dirname, "../skill");
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
|
|
52
|
+
function install(destPath, { force = false } = {}) {
|
|
53
|
+
const abs = resolve(cwd, destPath);
|
|
54
|
+
|
|
55
|
+
if (existsSync(abs)) {
|
|
56
|
+
if (!force) {
|
|
57
|
+
console.log(` ⚠️ Already exists: ${destPath}`);
|
|
58
|
+
console.log(` Use --force to overwrite.`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
rmSync(abs, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
mkdirSync(abs, { recursive: true });
|
|
65
|
+
cpSync(skillSrc, abs, { recursive: true });
|
|
66
|
+
console.log(` ✅ Installed → ${destPath}`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printHelp() {
|
|
71
|
+
console.log(`
|
|
72
|
+
Usage:
|
|
73
|
+
npx ${SKILL_NAME}-skill [options]
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--target <path> Install to a specific directory (relative to project root)
|
|
77
|
+
--force Overwrite if skill already exists
|
|
78
|
+
--help Show this message
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
npx fe-lwc-skill # auto-detect agent runtimes
|
|
82
|
+
npx fe-lwc-skill --target .claude/skills/fe-lwc
|
|
83
|
+
npx fe-lwc-skill --target .gemini/skills/fe-lwc --force
|
|
84
|
+
|
|
85
|
+
Supported runtimes (auto-detected):
|
|
86
|
+
${AGENT_RUNTIMES.map((r) => ` ${r.label.padEnd(18)} → ${r.skillPath}`).join("\n")}
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const args = process.argv.slice(2);
|
|
93
|
+
|
|
94
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
95
|
+
printHelp();
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const force = args.includes("--force");
|
|
100
|
+
const targetIdx = args.indexOf("--target");
|
|
101
|
+
|
|
102
|
+
console.log(`\n🔧 fe-lwc-skill installer\n`);
|
|
103
|
+
|
|
104
|
+
// ── Mode 1: explicit --target ─────────────────────────────────────────────────
|
|
105
|
+
if (targetIdx !== -1) {
|
|
106
|
+
const target = args[targetIdx + 1];
|
|
107
|
+
|
|
108
|
+
if (!target || target.startsWith("--")) {
|
|
109
|
+
console.error("❌ --target requires a path argument.");
|
|
110
|
+
console.error(" Example: --target .claude/skills/fe-lwc\n");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
install(target, { force });
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Mode 2: auto-detect ───────────────────────────────────────────────────────
|
|
119
|
+
const detected = AGENT_RUNTIMES.filter(({ markerDir }) => existsSync(join(cwd, markerDir)));
|
|
120
|
+
|
|
121
|
+
if (detected.length === 0) {
|
|
122
|
+
console.log("⚠️ No known agent config directory found in this project.");
|
|
123
|
+
console.log(" Looked for:", AGENT_RUNTIMES.map((r) => r.markerDir).join(", "));
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(" Use --target to install manually:");
|
|
126
|
+
console.log(` npx ${SKILL_NAME}-skill --target .claude/skills/${SKILL_NAME}\n`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`Found ${detected.length} agent runtime(s):\n`);
|
|
131
|
+
|
|
132
|
+
let installedCount = 0;
|
|
133
|
+
for (const runtime of detected) {
|
|
134
|
+
process.stdout.write(` [${runtime.label}] `);
|
|
135
|
+
const ok = install(runtime.skillPath, { force });
|
|
136
|
+
if (ok) installedCount++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log("");
|
|
140
|
+
if (installedCount > 0) {
|
|
141
|
+
console.log(`🎉 Done! Installed to ${installedCount} location(s).`);
|
|
142
|
+
console.log(` The agent will now load LWC rules from the skill directory.\n`);
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`Nothing was installed. Run with --force to overwrite existing installs.\n`);
|
|
145
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fe-lwc-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LWC Frontend Skill for AI Agents (Claude Code, Cursor, Gemini CLI, Continue.dev)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fe-lwc-skill": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"skill/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"lwc",
|
|
15
|
+
"salesforce",
|
|
16
|
+
"lightning-web-components",
|
|
17
|
+
"ai-skill",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"cursor",
|
|
20
|
+
"gemini"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fe-lwc
|
|
3
|
+
description: Generate Lightning Web Components (LWC) for Salesforce Experience Cloud portals and Lightning App Builder, following scalable architecture, performance optimization, and Salesforce platform best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LWC Frontend Development
|
|
7
|
+
|
|
8
|
+
Before writing any LWC code, you MUST read the relevant rule files below.
|
|
9
|
+
|
|
10
|
+
## Rules & Patterns
|
|
11
|
+
|
|
12
|
+
### Always read these first:
|
|
13
|
+
|
|
14
|
+
- Read `rules/design-token-dxp.md` and `rules/design-token-slds.md` before any UI styling
|
|
15
|
+
- Read `rules/data-lds-first.md` before any data fetching
|
|
16
|
+
- Read `rules/template-form-pattern.md` before building forms
|
|
17
|
+
- **Creating a new LWC?** Always scaffold with the CLI first — replace `<componentName>` with the actual component name from the prompt:
|
|
18
|
+
```bash
|
|
19
|
+
sf lightning generate component --type lwc --name <componentName> --output-dir force-app/main/default/lwc
|
|
20
|
+
```
|
|
21
|
+
- For every TypeScript LWC component created, always add a .gitignore entry for the generated JavaScript file.
|
|
22
|
+
|
|
23
|
+
### Read based on task:
|
|
24
|
+
|
|
25
|
+
- Wire vs imperative: read `rules/data-wire-vs-imperative.md`
|
|
26
|
+
- Custom events: read `rules/events-custom-event.md`
|
|
27
|
+
- LMS: read `rules/events-lms.md`
|
|
28
|
+
- Template directives: read `rules/template-directives.md`
|
|
29
|
+
- DOM querying: read `rules/template-dom-querying.md`
|
|
30
|
+
- Track/immutable: read `rules/template-track-immutable.md`
|
|
31
|
+
- Error handling: read `rules/data-error-handling.md`
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# When to Apply
|
|
36
|
+
|
|
37
|
+
Reference these guidelines when:
|
|
38
|
+
|
|
39
|
+
- Writing a new LWC component for an **Experience Cloud portal** (LWR runtime)
|
|
40
|
+
- Writing a custom component for a **Salesforce Lightning App** (App Builder)
|
|
41
|
+
- Implementing data fetching with `@wire` or imperative Apex calls
|
|
42
|
+
- Reviewing or refactoring existing LWC code for consistency and performance
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# Core Principles
|
|
47
|
+
|
|
48
|
+
All Lightning Web Components should follow these engineering principles to ensure scalability, maintainability, and platform consistency.
|
|
49
|
+
|
|
50
|
+
- **LDS First** — Prefer Lightning Data Service (`lightning/uiRecordApi`) for standard CRUD operations before writing Apex controllers.
|
|
51
|
+
- **LWR Optimized** — Components must be optimized for Lightning Web Runtime (LWR).
|
|
52
|
+
- **Platform Native First** — Always prefer standard Salesforce capabilities before implementing custom solutions.
|
|
53
|
+
- **Performance Focused** — Avoid unnecessary re-renders, excessive DOM queries, and large reactive states.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Quick Reference
|
|
58
|
+
|
|
59
|
+
## Data Patterns
|
|
60
|
+
|
|
61
|
+
| Rule | Description |
|
|
62
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `data-lds-first` | Use `lightning/uiRecordApi` for standard CRUD before reaching for Apex |
|
|
64
|
+
| `data-wire-vs-imperative` | Use `@wire` for declarative data binding; use imperative calls when you need explicit control (e.g. on user action) |
|
|
65
|
+
| `data-error-handling` | Always handle wire/Apex errors using the `reduceErrors` utility; never silently swallow errors |
|
|
66
|
+
|
|
67
|
+
## Template Standards
|
|
68
|
+
|
|
69
|
+
| Rule | Description |
|
|
70
|
+
| -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| `template-directives` | Use `lwc:if` / `lwc:elseif` / `lwc:else` / `for:each` + `for:item` — never the deprecated `if:true` / `if:false` |
|
|
72
|
+
| `template-track-immutable` | Avoid unnecessary `@track`; trigger reactivity through immutable updates (`[...arr]`, `{...obj}`) |
|
|
73
|
+
| `template-form-pattern` | Use a single `handleChange` method keyed on `event.target.name`; keep form state in one object |
|
|
74
|
+
| `template-dom-querying` | Target elements with `data-id` attributes and `this.template.querySelector()`; avoid class/tag selectors |
|
|
75
|
+
|
|
76
|
+
## Component Communication
|
|
77
|
+
|
|
78
|
+
| Rule | Description |
|
|
79
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `events-custom-event` | Dispatch custom events with explicit `bubbles` and `composed` flags; use kebab-case or camelCase event names |
|
|
81
|
+
| `events-lms` | Use Lightning Message Service (LMS) for cross-region communication on App Builder and Experience Cloud pages; avoid direct component coupling |
|
|
82
|
+
|
|
83
|
+
## Design System
|
|
84
|
+
|
|
85
|
+
| Rule | Description |
|
|
86
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| `design-token-dxp` | Use `dxp-text-*` classes for typography in HTML templates, `--dxp-s-text-*` hooks in component CSS, and `--dxp-g-*` color hooks — never hardcode hex values; define site-wide overrides in Head Markup `:root` only |
|
|
88
|
+
| `design-token-slds` | Apply SLDS utility classes for spacing and layout before writing any custom CSS |
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# data-error-handling
|
|
2
|
+
|
|
3
|
+
**Impact: HIGH**
|
|
4
|
+
|
|
5
|
+
Unhandled errors are the most common silent failure in LWC. In LWC, `@wire` errors were frequently ignored — the component simply rendered nothing, leaving the user with a blank screen and no explanation. This rule covers how to handle errors consistently across wire adapters, imperative Apex calls, and form validation.
|
|
6
|
+
|
|
7
|
+
> **Rule:**
|
|
8
|
+
>
|
|
9
|
+
> - Always check `error` on every `@wire` result — never assume `data` is always present.
|
|
10
|
+
> - Handle the loading state too — when both `data` and `error` are `undefined`, the wire is still pending.
|
|
11
|
+
> - Use `reduceErrors` to extract readable messages from any Salesforce error shape.
|
|
12
|
+
> - Show a **toast** for Apex/server errors.
|
|
13
|
+
> - Show **inline messages** for form validation errors.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The `reduceErrors` utility
|
|
18
|
+
|
|
19
|
+
Salesforce errors can come in many shapes — a wire `FetchResponse`, an imperative `AuraHandledException`, or a list of field-level errors from `createRecord`. `reduceErrors` normalizes all of them into a simple string array.
|
|
20
|
+
|
|
21
|
+
Copy this utility once into your project at `lwc/utils/errorUtils.ts`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// utils/errorUtils.ts
|
|
25
|
+
|
|
26
|
+
interface SalesforceError {
|
|
27
|
+
body?: { message?: string } | Array<{ message: string }>;
|
|
28
|
+
message?: string;
|
|
29
|
+
statusText?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reduces one or more LDS errors into a string array of error messages.
|
|
34
|
+
*/
|
|
35
|
+
export function reduceErrors(errors: SalesforceError | SalesforceError[]): string[] {
|
|
36
|
+
if (!Array.isArray(errors)) {
|
|
37
|
+
errors = [errors];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return errors
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((error) => {
|
|
43
|
+
if (Array.isArray(error.body)) {
|
|
44
|
+
return error.body.map((e) => e.message);
|
|
45
|
+
}
|
|
46
|
+
if (error.body && typeof error.body.message === "string") {
|
|
47
|
+
return error.body.message;
|
|
48
|
+
}
|
|
49
|
+
if (typeof error.message === "string") {
|
|
50
|
+
return error.message;
|
|
51
|
+
}
|
|
52
|
+
return error.statusText ?? "Unknown error";
|
|
53
|
+
})
|
|
54
|
+
.reduce<string[]>((acc, val) => acc.concat(val), [])
|
|
55
|
+
.filter((message): message is string => !!message);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Case 1: Ignoring `@wire` errors (and missing loading state)
|
|
62
|
+
|
|
63
|
+
**Incorrect — blank screen when wire fails; no loading state:**
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// patientSummary.ts
|
|
67
|
+
import { LightningElement, api, wire } from "lwc";
|
|
68
|
+
import getPatientSummary from "@salesforce/apex/PatientController.getPatientSummary";
|
|
69
|
+
|
|
70
|
+
// @ts-ignore — decorator typing limitation in current Developer Preview
|
|
71
|
+
export default class PatientSummary extends LightningElement {
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
@api recordId: string;
|
|
74
|
+
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
@wire(getPatientSummary, { recordId: "$recordId" })
|
|
77
|
+
summary: { data?: PatientSummary; error?: unknown };
|
|
78
|
+
|
|
79
|
+
// summary.error is never checked
|
|
80
|
+
// loading state (data === undefined && error === undefined) never handled
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Correct — handle loading, data, and error states:**
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// patientSummary.ts
|
|
88
|
+
import { LightningElement, api, wire } from "lwc";
|
|
89
|
+
import { ShowToastEvent } from "lightning/platformShowToastEvent";
|
|
90
|
+
import getPatientSummary from "@salesforce/apex/PatientController.getPatientSummary";
|
|
91
|
+
import { reduceErrors } from "c/utils";
|
|
92
|
+
|
|
93
|
+
interface PatientSummaryRecord {
|
|
94
|
+
Name: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
export default class PatientSummary extends LightningElement {
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
@api recordId: string;
|
|
101
|
+
|
|
102
|
+
summary: PatientSummaryRecord | null = null;
|
|
103
|
+
isLoading: boolean = true;
|
|
104
|
+
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
@wire(getPatientSummary, { recordId: "$recordId" })
|
|
107
|
+
public handleSummary({ data, error }: { data?: PatientSummaryRecord; error?: unknown }): void {
|
|
108
|
+
this.isLoading = false;
|
|
109
|
+
if (data) {
|
|
110
|
+
this.summary = data;
|
|
111
|
+
} else if (error) {
|
|
112
|
+
this.dispatchEvent(
|
|
113
|
+
new ShowToastEvent({
|
|
114
|
+
title: "Error loading patient",
|
|
115
|
+
message: reduceErrors(error).join(", "),
|
|
116
|
+
variant: "error",
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<!-- patientSummary.html -->
|
|
126
|
+
<template>
|
|
127
|
+
<template lwc:if="{isLoading}">
|
|
128
|
+
<lightning-spinner alternative-text="Loading"></lightning-spinner>
|
|
129
|
+
</template>
|
|
130
|
+
|
|
131
|
+
<template lwc:elseif="{summary}">
|
|
132
|
+
<p>{summary.Name}</p>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<template lwc:else>
|
|
136
|
+
<p>No patient data available.</p>
|
|
137
|
+
</template>
|
|
138
|
+
</template>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Case 2: Imperative Apex call without error handling
|
|
144
|
+
|
|
145
|
+
**Incorrect — no try/catch, unhandled promise rejection:**
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// appointmentSearch.ts
|
|
149
|
+
import { LightningElement } from "lwc";
|
|
150
|
+
import searchAppointments from "@salesforce/apex/AppointmentController.searchAppointments";
|
|
151
|
+
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
export default class AppointmentSearch extends LightningElement {
|
|
154
|
+
appointments: unknown[] = [];
|
|
155
|
+
|
|
156
|
+
public async handleSearch(): Promise<void> {
|
|
157
|
+
// If this throws, nothing catches it — silent failure.
|
|
158
|
+
this.appointments = await searchAppointments({ term: this.searchTerm });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Correct — try/catch with toast on error:**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// appointmentSearch.ts
|
|
167
|
+
import { LightningElement } from "lwc";
|
|
168
|
+
import { ShowToastEvent } from "lightning/platformShowToastEvent";
|
|
169
|
+
import searchAppointments from "@salesforce/apex/AppointmentController.searchAppointments";
|
|
170
|
+
import { reduceErrors } from "c/utils";
|
|
171
|
+
|
|
172
|
+
interface AppointmentRecord {
|
|
173
|
+
Id: string;
|
|
174
|
+
Name: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
export default class AppointmentSearch extends LightningElement {
|
|
179
|
+
searchTerm: string = "";
|
|
180
|
+
appointments: AppointmentRecord[] = [];
|
|
181
|
+
isLoading: boolean = false;
|
|
182
|
+
|
|
183
|
+
public handleChange(event: Event): void {
|
|
184
|
+
this.searchTerm = (event.target as HTMLInputElement).value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async handleSearch(): Promise<void> {
|
|
188
|
+
this.isLoading = true;
|
|
189
|
+
try {
|
|
190
|
+
this.appointments = await searchAppointments({ term: this.searchTerm });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.dispatchEvent(
|
|
193
|
+
new ShowToastEvent({
|
|
194
|
+
title: "Search failed",
|
|
195
|
+
message: reduceErrors(error).join(", "),
|
|
196
|
+
variant: "error",
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
} finally {
|
|
200
|
+
this.isLoading = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Case 3: Form validation — inline error, not toast
|
|
209
|
+
|
|
210
|
+
**Incorrect — toast for a form validation error:**
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// bookingForm.ts
|
|
214
|
+
public async handleSubmit(event: Event): Promise<void> {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
if (!this.formState.date) {
|
|
217
|
+
this.dispatchEvent(
|
|
218
|
+
new ShowToastEvent({ title: 'Date is required', variant: 'error' })
|
|
219
|
+
);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Correct — inline error using `setCustomValidity` + `reportValidity`:**
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// bookingForm.ts
|
|
229
|
+
import { LightningElement } from "lwc";
|
|
230
|
+
|
|
231
|
+
interface BookingFormState {
|
|
232
|
+
date: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// @ts-ignore
|
|
236
|
+
export default class BookingForm extends LightningElement {
|
|
237
|
+
formState: BookingFormState = { date: "" };
|
|
238
|
+
|
|
239
|
+
public handleChange(event: Event): void {
|
|
240
|
+
const target = event.target as HTMLInputElement;
|
|
241
|
+
this.formState = { ...this.formState, [target.name]: target.value };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public handleSubmit(event: Event): void {
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
|
|
247
|
+
const dateInput = this.template.querySelector<HTMLInputElement>('[data-id="date-input"]');
|
|
248
|
+
if (!dateInput) return;
|
|
249
|
+
|
|
250
|
+
if (!this.formState.date) {
|
|
251
|
+
dateInput.setCustomValidity("Please select an appointment date.");
|
|
252
|
+
dateInput.reportValidity();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
dateInput.setCustomValidity("");
|
|
257
|
+
this.submitBooking();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private submitBooking(): void {
|
|
261
|
+
// submit logic
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<!-- bookingForm.html -->
|
|
268
|
+
<template>
|
|
269
|
+
<form onsubmit="{handleSubmit}">
|
|
270
|
+
<lightning-input
|
|
271
|
+
data-id="date-input"
|
|
272
|
+
name="date"
|
|
273
|
+
type="date"
|
|
274
|
+
label="Appointment Date"
|
|
275
|
+
value="{formState.date}"
|
|
276
|
+
onchange="{handleChange}">
|
|
277
|
+
</lightning-input>
|
|
278
|
+
<lightning-button type="submit" variant="brand" label="Book"></lightning-button>
|
|
279
|
+
</form>
|
|
280
|
+
</template>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Quick Decision Guide
|
|
286
|
+
|
|
287
|
+
| Situation | Error Pattern |
|
|
288
|
+
| ------------------------------------------ | --------------------------------------------- |
|
|
289
|
+
| `@wire` result — data/error both undefined | Show loading spinner |
|
|
290
|
+
| `@wire` returns error | Wire function handler + toast |
|
|
291
|
+
| Imperative Apex throws | `try/catch` + toast |
|
|
292
|
+
| Form field fails validation | `setCustomValidity` + `reportValidity` inline |
|
|
293
|
+
| Multiple errors from LDS | `reduceErrors(error).join(', ')` |
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
**Reference:** [Handle Errors in Lightning Data Service — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/data-error.html)
|