primitives
Forms & Rules
A guided example with multiple files and a prepared interactive session.
Example overview
This prerendered document exposes Forms & Rules as navigable HTML. The interactive playground experience is enhanced later by the client bundle.
Source preview
import formsStyles from "./forms.styles.css";
import {
PickComponent,
PickRender,
Reactive,
Listen,
Services,
type PickViewActions,
} from "pick-components";
import {
type FieldValidationRules,
type RegistrationFormRules,
type RegistrationFormSession,
} from "./forms.rules-service.js";
@PickRender({
selector: "forms-example",
initializer: () => Services.get("RegistrationFormInitializer"),
skeleton: `
<section class="panel" role="status" aria-live="polite">
<p class="eyebrow">Rendering lifecycle</p>
<h3>Forms & Rules</h3>
<p aria-busy="true">Loading validation rules...</p>
</section>
`,
errorTemplate:
'<p role="alert">Validation rules could not be loaded before render.</p>',
styles: formsStyles,
template: `
<section class="panel">
<header>
<p class="eyebrow">Rendering lifecycle</p>
<h3>Forms & Rules</h3>
<p class="intro">
A service provides validation rules before the first render.
[[RULES.*]] applies them to the inputs.
</p>
</header>
<pick-select>
<on condition="{{submitted}}">
<div class="success" role="alert">
<p>Form submitted successfully.</p>
<pick-action action="resetForm"><button type="button">Start over</button></pick-action>
</div>
</on>
<otherwise>
<form id="regForm" novalidate>
<div class="field">
<label for="usernameField">Username <span class="required">*</span></label>
<input id="usernameField" type="text" placeholder="Username" aria-describedby="usernameHint" [[RULES.username]] />
<small id="usernameHint" class="hint">{{usernameHint}}</small>
</div>
<div class="field">
<label for="emailField">Email <span class="required">*</span></label>
<input id="emailField" type="email" placeholder="Email" aria-describedby="emailHint" [[RULES.email]] />
<small id="emailHint" class="hint">{{emailHint}}</small>
</div>
<div class="field">
<label for="passwordField">Password <span class="required">*</span></label>
<input id="passwordField" type="password" placeholder="Password" aria-describedby="passwordHint" [[RULES.password]] />
<small id="passwordHint" class="hint">{{passwordHint}}</small>
</div>
<p class="status"><strong>{{validCount}}</strong>/3 valid</p>
<button type="submit" disabled="{{!isValid}}">Register</button>
</form>
</otherwise>
</pick-select>
</section>
`,
})
export class FormsExample extends PickComponent {
rules: RegistrationFormRules = {};
@Reactive isValid = false;
@Reactive validCount = 0;
@Reactive submitted = false;
get usernameHint(): string {
return this.describeRule(this.rules.username);
}
get emailHint(): string {
return this.describeRule(this.rules.email);
}
get passwordHint(): string {
return this.describeRule(this.rules.password);
}
hydrate(session: RegistrationFormSession): void {
this.rules = session.rules;
this.resetRuleState();
}
getViewActions(): PickViewActions {
return {
resetForm: () => this.resetRuleState(),
};
}
@Listen("focusout")
onFieldBlur(event: Event): void {
const field = event.target as HTMLElement;
if (field.tagName === "INPUT") {
(field as HTMLInputElement).classList.add("touched");
}
}
@Listen("input")
onFormInput(event: Event): void {
const form = (event.target as HTMLElement).closest<HTMLFormElement>("form");
if (form) {
this.evaluateFormRules(form);
}
}
@Listen("submit")
handleRegister(event: Eve
...