diff --git a/.github/skills/webui-dev/SKILL.md b/.github/skills/webui-dev/SKILL.md new file mode 100644 index 00000000..e04d91c5 --- /dev/null +++ b/.github/skills/webui-dev/SKILL.md @@ -0,0 +1,393 @@ +--- +name: webui-dev +description: Build interactive WebUI example apps with FAST-HTML hydration, template syntax, and component patterns. +--- + +# WebUI App Development with FAST-HTML + +Use this skill when building or modifying example apps under `examples/app/`. + +WebUI server-renders HTML at request time. FAST-HTML hydrates the pre-rendered DOM on the client, making it interactive without a full re-render. + +## Project structure + +Every example app follows this layout: + +``` +examples/app// +├── src/ +│ ├── index.html # HTML shell + CSS design tokens +│ ├── index.ts # Hydration bootstrap +│ └── / # One directory per component +│ ├── .ts +│ ├── .html +│ └── .css +├── data/ +│ └── state.json # Initial state for SSR +├── package.json +└── tsconfig.json +``` + +### package.json + +```json +{ + "type": "module", + "scripts": { + "start:client": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --sourcemap --watch", + "start:server": "cargo run -p webui-cli -- start ./src --state ./data/state.json --plugin=fast --servedir ./dist --port 3001 --watch", + "start": "cargo xtask dev " + }, + "devDependencies": { + "@microsoft/fast-element": "catalog:", + "@microsoft/fast-html": "catalog:", + "esbuild": "catalog:", + "tslib": "catalog:", + "typescript": "catalog:" + } +} +``` + +Use `catalog:` for all dependency versions — they resolve from `pnpm-workspace.yaml`. + +### tsconfig.json + +Required settings for FAST-HTML: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true, + "useDefineForClassFields": false + } +} +``` + +`useDefineForClassFields: false` is **mandatory** — FAST decorators rely on legacy class field behavior. + +## Template syntax (HTML) + +### Signal interpolation — `{{}}` + +Binds server-side state values into HTML. Processed by the WebUI parser at build time. + +```html + +

{{userName}}

+ + +{{linkText}} + + +{{user.profile.name}} + + + +
{{item.title}}
+
+ + +
...
+``` + +### Raw (unescaped) interpolation — `{{{}}}` + +Passes HTML through without entity encoding: + +```html +
{{{rawHtmlContent}}}
+``` + +### Loop rendering — `` + +```html + + + +``` + +- Loop variable is scoped to the `` block and its children. +- Nested loops can access outer loop items via their moniker. +- Array length: `{{items.length}}`. Array indexing: `{{items.0.name}}`. + +### Conditional rendering — `` + +```html + +
Shown when truthy
+
+ + +
Comparisons work
+
+ + +
Logical operators work
+
+``` + +Supported: `==`, `!=`, `>`, `<`, `>=`, `<=`, `&&`, `||`, `!`. Max 5 operators per expression. + +### Boolean attributes — `?` + +Rendered only when the bound value is truthy: + +```html + +
...
+``` + +### Complex (pass-through) attributes — `:` + +For structured data that passes through as-is: + +```html + +``` + +## FAST-HTML component patterns + +### Component class + +Every component extends `RenderableFASTElement(FASTElement)`: + +```typescript +import { FASTElement, attr, observable } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class MyComponent extends RenderableFASTElement(FASTElement) { + @attr name!: string; // HTML attribute (string, reflected) + @observable items!: ItemData[]; // Reactive property (triggers re-render) + + inputRef!: HTMLInputElement; // Template ref target (via f-ref) + + async prepare(): Promise { + // Hydration hook — read state from pre-rendered DOM + } + + connectedCallback(): void { + super.connectedCallback(); + // Post-hydration setup (event listeners, focus, etc.) + } +} + +MyComponent.defineAsync({ + name: 'my-component', + templateOptions: 'defer-and-hydrate', +}); +``` + +### `@attr` — HTML attributes + +- Reflected to/from the HTML attribute on the element. +- **Use `!:` (no initializer)** for fields set in `prepare()`. Class field initializers run AFTER `super()`, overwriting values set during hydration. +- Hyphenated HTML attributes map via `@attr({ attribute: 'display-value' })`. + +### `@observable` — Reactive properties + +- Triggers FAST template re-rendering when changed. +- Use for data that drives `` loops or conditional display. +- **Use `!:` (no initializer)** for fields set in `prepare()`. + +### `prepare()` — Hydration hook + +Called during hydration to initialize component state from the pre-rendered DOM. This is **the** place to read server-rendered data: + +```typescript +async prepare(): Promise { + // Read @attr values from HTML attributes (initializers haven't run yet) + this.mode = this.getAttribute('mode') || 'default'; + + // Read child elements rendered by SSR + const items: ItemData[] = []; + for (const el of this.shadowRoot!.querySelectorAll('my-item')) { + items.push({ + id: el.getAttribute('id') || '', + title: el.getAttribute('title') || '', + }); + } + this.items = items; +} +``` + +Rules: + +- Always read `@attr` values from `this.getAttribute()` — decorators initialize AFTER `prepare()`. +- Read child element data from `this.shadowRoot.querySelectorAll()`. +- Guard with `if (!this.shadowRoot) return;` when appropriate. +- Avoid `` loops over `@observable` string arrays — use object arrays or static HTML. + +### Component template (HTML) + +The `