Build a case-decision control used by two permit-review applications. Start with a server-rendered select named decision for case 47 so the review can be submitted before the custom element definition loads. The optional element presents policy help and a clearer selected state. It receives the case ID and current decision as attributes, accepts a slotted heading and help link, and emits one case-decision event with only the stable code and case ID. Its public names, event version, and styling hooks are documented. The host validates authorization and persists the decision. The component does not fetch private case text or treat an event as permission. When enhanced, an autonomous form-associated element writes one value through ElementInternals; the fallback control must no longer be a second successful control. The component handles reset, disabled state, and invalid selection. It starts any subscriptions only while connected, makes setup idempotent, and releases listeners on removal. If the host moves the same node, a reconnect must not double-submit. Styling uses two inherited tokens and a named summary part, with defaults that remain readable in an unfamiliar host. A shared static stylesheet is used only for global rules. The first application may theme one card without changing the others. The project succeeds when the server receives the same stable code from the plain and enhanced paths, not merely when the element looks correct.
Project: interoperable case-decision control
Build contract
- Submit one stable code through both the plain and enhanced paths.
- Keep slotted content and events usable without internal selectors.
- Survive delayed definition, repeated connection, and host replacement.
- Preserve focus and visible state through reset, errors, and theme changes.
Implementation checkpoint
function acceptedDecision(caseRecord, eventDetail) {
const allowed = new Set(['approve', 'return', 'defer']);
return eventDetail.version === 2 && eventDetail.caseId === caseRecord.id &&
allowed.has(eventDetail.code) && caseRecord.editable;
}
console.log(acceptedDecision({ id: 47, editable: true }, { version: 2, caseId: 62, code: 'approve' }));
// Output: falseCost and boundaries
A three-choice decision is O(1) to validate locally; the server check is still required. The extra component bundle, shadow tree, and integration tests add weight relative to a native select. In a list of n cases, one listener or observer per card consumes O(n) resources. A per-card polling loop is a larger network and battery cost, so prefer one host-owned feed. A shared sheet avoids repeated static CSS parsing, but changing its rules can invalidate styles across every adopter. Record bundle size, mount time for 83 cards, keyboard completion, and failed submissions before replacing the plain form as the default path.
Failure drill
Block the component script and submit from the native select. Load the definition late, then submit again and inspect FormData for exactly one decision field. Change the selected case while a delayed event is in flight and reject its stale case ID. Move the element four times; prove there is still one active listener and one server mutation per click. Replace the slotted help link without rebuilding the card. Reset and disable the form, then test a server-rejected choice. Check focus after an error and after the card is removed. Apply a local dark theme to one of 83 cards; the other cards must retain their values. Run keyboard and screen-reader checks rather than assuming shadow markup is usable.
Acceptance checks
- One named field reaches the server with a stable code.
- The pre-upgrade and failed-script paths can finish the review.
- Moving the element does not multiply subscriptions or mutations.
- The host can theme one card and operate it without private selectors.
Common Mistakes
- Leaving both enhanced and fallback controls successful.
- Assuming a composed event grants server authority.
- Using the shadow root as a privacy boundary.
Related lessons
Custom Element Upgrade and Reconnection; Shadow Slots, Events, and Focus Contracts; Form-Associated Custom Controls and Native Fallback; Shadow Style Tokens, Parts, and Shared Sheets.
