Skip to content

feat(forms, component): expand into batteries-included tier (1.13.0) - #95

Merged
JosunLP merged 24 commits into
devfrom
copilot/improve-expand-form-component-endpoint
May 20, 2026
Merged

feat(forms, component): expand into batteries-included tier (1.13.0)#95
JosunLP merged 24 commits into
devfrom
copilot/improve-expand-form-component-endpoint

Conversation

Copilot AI commented May 20, 2026

Copy link
Copy Markdown
Contributor

Turns @bquery/bquery/forms and @bquery/bquery/component from solid building blocks into a true batteries-included tier — covering everything users typically reach for — while preserving zero deps, tree-shakeability, and the core → reactive → view/forms/component dependency rule.

Forms (src/forms/*)

  • Validators: integer, numeric, between, length, oneOf, notOneOf, arrayOf, requiredIf, requiredUnless, dateAfter, dateBefore, validDate, fileSize, fileType (validDate renamed from the planned isDate to avoid collision with the existing core type guard), plus combinators compose, all, not, withMessage.
  • Field state: lifted isValidating/isFocused/dirtySince onto every FormField; added focus/blur, setValue(v, { touch, validate, silent }), setError/clearError, a disabled signal that excludes the field from validation, per-field validateOn/debounceMs, and parse/format on FieldConfig.
  • Form state: submitCount, lastSubmittedAt, submitError, aggregated isValidating/isPristine, touchAll/untouchAll, resetField, resetErrors, getDirtyValues, subscribe, plus onSubmitError/onSubmitSuccess/validationStrategy/mode: 'all' | 'first' on FormConfig.
  • Field arrays: createFieldArray({ initial, factory, validators }) with add/remove/move/insert/clear.
  • DOM bindings: bindField and bindForm (auto-discovers [name], wires submit, marks aria-invalid, configurable errorSlot mapper); both return cleanup.
  • Composables: scope-aware useForm/useField/useFieldArray that auto-dispose with the owning component.
  • Schema: fluent schema({ name: field<string>().required().min(2), … }) over the existing validator factories.
  • Serialization & SSR: form.toJSON/toFormData/snapshot/restore, plus serializeFormState/readSerializedFormState/hydrateForm built on src/ssr/escape.ts.

Component (src/component/*)

  • Refs & slots: useRef<T>() (auto-cleared on disconnect), useSlot (reactive Signal<Element[]>), hasSlot, slotText.
  • Delegated events: on/onClick/onInput/onChange/onSubmit + bindDelegatedEvents(host). Handlers live in a module map keyed by opaque IDs; templates only carry data-bq-on-<event>="<id>", so no eval, no inline on*, and the existing sanitizer accepts the markup unchanged. One delegated listener per event type per host.
  • DI: provide/inject/injectionKey<T>() over the composed event path; ships a built-in formContextKey.
  • Lifecycle: additive beforeUnmount, errorBoundary(error, info) returning fallback markup, scope-tracked whenIdle(fn).
  • Async: useAsync(fn){ data, error, loading, refresh } with AbortController cancellation on disconnect or re-invocation.
  • Reactive props: instance setProp(name, value)/getProp(name) for objects/arrays/callbacks that bypass attribute serialization and trigger a re-render.
  • Styles: css tagged template returning a ComponentStyles payload, shared across instances via document.adoptedStyleSheets when constructable stylesheets are available (falls back to <style>); interpolations are CSS-escaped.
  • Keyed lists: keyedList(items, key, renderItem) injects data-bq-key into the first opening tag of each item; reconcileKeyed(container) reorders existing children to match key order.

Example

import { component, html, css, useRef, useAsync, onClick, provide, injectionKey } from '@bquery/bquery/component';
import { useForm, required, email, between, schema, field, bindForm } from '@bquery/bquery/forms';

const ThemeKey = injectionKey<{ dark: boolean }>('theme');

component('signup-form', {
  styles: css`:host { display: block; } .err { color: crimson; }`,
  connected() {
    provide(this, ThemeKey, { dark: true });

    const form = useForm({
      fields: schema({
        name:  field<string>().required().min(2),
        email: field<string>().required().email(),
        age:   field<number>().required().integer().between(18, 120),
      }),
      validationStrategy: 'onBlur',
      mode: 'all',
      onSubmit: async (values) => fetch('/api/signup', { method: 'POST', body: JSON.stringify(values) }),
      onSubmitError: (err) => console.error(err),
    });

    queueMicrotask(() => bindForm(form, this.shadowRoot!.querySelector('form')!));
  },
  render: () => html`
    <form>
      <input name="name"  /> <span data-bq-error-for="name"  class="err"></span>
      <input name="email" /> <span data-bq-error-for="email" class="err"></span>
      <input name="age" type="number" /> <span data-bq-error-for="age" class="err"></span>
      <button type="submit">Sign up</button>
    </form>
  `,
});

Wiring & docs

  • src/full.ts and both module barrels re-export every new runtime + type surface; bun run check:full-bundle continues to enforce drift detection.
  • New test files: tests/forms-extensions.test.ts, tests/component-extensions.test.ts.
  • docs/guide/forms.md and docs/guide/components.md extended with “What's new in 1.13.0” sections.
  • Bumped to 1.13.0; AGENT.md, llms.txt, copilot-instructions, .cursorrules, .clinerules, README, and CHANGELOG synced (bun run check:ai-guidance passes).

Notes for reviewers

  • All additions are backwards-compatible; existing forms and components keep working unchanged. The single behavioral touch outside new files is restoring the pre-existing component hasMounted semantics after adding beforeUnmount — covered by the existing reconnect tests.
  • The forms validator helpers compose/all/not/withMessage/arrayOf cast their return to Validator<T> because the type is a SyncValidator | AsyncValidator union and a single function value cannot inhabit both members of the union purely structurally; runtime behavior is correct in both modes.
  • Delegated events rely on data-bq-* data attributes that the default sanitizer already permits; no sanitizer changes were needed.

@JosunLP
JosunLP marked this pull request as ready for review May 20, 2026 06:09
Copilot AI review requested due to automatic review settings May 20, 2026 06:09
@github-actions github-actions Bot added component Changes to the component module docs Changes to the documentation tests Chenges to the tests build Changes to the build and meta files github Changes to the github meta data files forms Changes to the forms module labels May 20, 2026
Comment thread src/forms/create-form.ts Outdated
initialNotifyRun = false;
return;
}
if (suppressNotify) {
@JosunLP
JosunLP changed the base branch from main to dev May 20, 2026 06:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Expands the @bquery/bquery/forms and @bquery/bquery/component modules into a more “batteries-included” tier for the 1.13.0 release, adding new validators, form/field APIs, DOM bindings, SSR helpers, and multiple component ergonomics (refs/slots/DI/events/styles/async/lifecycle), plus re-export wiring, docs, and tests.

Changes:

  • Forms: adds new validators + combinators, schema builder, field arrays, DOM bindings, SSR serialize/hydrate helpers, and richer field/form state & APIs.
  • Component: adds delegated event binding, DI helpers, refs/slots, async helpers, css tagged template + adopted styles support, keyed list helpers, and lifecycle additions.
  • Release wiring: bumps version to 1.13.0 and updates /full exports, docs, changelog, and repo guidance snapshots; adds new test suites.

Reviewed changes

Copilot reviewed 34 out of 35 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tests/forms.test.ts Updates an internal-subscriber-count assertion after new field effects.
tests/forms-extensions.test.ts New tests covering added forms validators, field/form extensions, bindings, schema, and SSR helpers.
tests/component-extensions.test.ts New tests covering component additions (refs/slots/DI/events/css/keyed lists/async/lifecycle/props).
src/full.ts Re-exports new forms + component runtime/type surface for the full bundle.
src/forms/validators.ts Adds new validators (numeric/date/file/etc.) and combinators (compose/all/not/withMessage).
src/forms/use-field.ts Extends standalone fields with focus/disabled/dirtySince + helper methods.
src/forms/types.ts Extends forms public types (field/form state, arrays, bindings, SSR snapshots, strategies).
src/forms/ssr.ts Adds inline-JSON serialization + DOM read/hydrate helpers for SSR.
src/forms/schema.ts Adds fluent field() / schema() builder over validator factories.
src/forms/index.ts Exposes the expanded forms surface via the module barrel.
src/forms/field-array.ts Adds reactive field array helper with mutation + validation hooks.
src/forms/create-form.ts Expands form runtime: new state signals, subscribe/snapshot/toFormData, auto-validation wiring.
src/forms/composables.ts Adds component-scope-aware useForm/useField/useFieldArray.
src/forms/bind.ts Adds bindField / bindForm DOM bridge for form fields.
src/component/types.ts Extends component types (props API, styles payload type, lifecycle/error boundary additions).
src/component/slots.ts Adds reactive slot helpers (useSlot, hasSlot, slotText).
src/component/refs.ts Adds useRef() utility that auto-clears on disconnect.
src/component/keyed-list.ts Adds keyedList renderer + reconcileKeyed reordering helper.
src/component/inject.ts Adds DI primitives (provide/inject/injectionKey) + formContextKey.
src/component/index.ts Re-exports new component APIs/types from the public barrel.
src/component/events.ts Adds sanitizer-safe delegated event binding (on*, bindDelegatedEvents).
src/component/css.ts Adds css tagged template + adopted stylesheet support + type guard.
src/component/component.ts Integrates new styles payload handling, setProp/getProp, beforeUnmount, and render error boundary.
src/component/async.ts Adds whenIdle and useAsync helpers with scope disposal + aborting.
README.md Updates “New in …” section to 1.13.0 highlights.
package.json Bumps version to 1.13.0.
llms.txt Updates baseline version and adds 1.13.0 highlights.
docs/guide/forms.md Adds “What’s new in 1.13.0” section for forms.
docs/guide/components.md Adds “What’s new in 1.13.0” section for components.
CHANGELOG.md Adds 1.13.0 release notes.
bun.lock Updates dev dependency versions (storybook/eslint/bun-types/vite).
AGENT.md Updates baseline version and adds 1.13.0 highlights.
.github/copilot-instructions.md Updates baseline version and adds 1.13.0 highlights.
.cursorrules Updates baseline version.
.clinerules Updates baseline version.
Comments suppressed due to low confidence (1)

src/forms/ssr.ts:45

  • The serializeFormState docstring says the script writes to window.__BQUERY_FORMS__[id], but the implementation returns a <script type="application/json" data-bq-form="..."> containing raw JSON and never writes to window. This mismatch will confuse users; update the docs to describe the actual DOM-embedded JSON approach (and/or change implementation).
 * Serialize a form snapshot to an inline `<script>` tag suitable for embedding
 * in SSR-rendered HTML. The script writes the payload to
 * `window.__BQUERY_FORMS__[id]` where it can be read on the client by
 * {@link readSerializedFormState} and applied via {@link Form.restore}.
 *
 * @param id - Stable identifier for this form
 * @param snapshot - Snapshot returned by {@link Form.snapshot}
 * @returns A complete `<script>` tag string
 *
 * @example
 * ```ts
 * import { serializeFormState } from '@bquery/bquery/forms';
 *
 * const html = `
 *   <form id="register">...</form>
 *   ${serializeFormState('register', form.snapshot())}
 * `;
 * ```
 */
export const serializeFormState = <T extends Record<string, unknown>>(
  id: string,
  snapshot: FormSnapshot<T>
): string => {
  const safeId = String(id).replace(/[^\w-]/g, '');
  const json = JSON.stringify(snapshot);
  const payload = escapeForScript(json);
  return `<script type="application/json" data-bq-form="${safeId}">${payload}</script>`;
};

Comment thread src/component/css.ts Outdated
Comment thread src/forms/create-form.ts
Comment thread src/forms/create-form.ts
Comment thread src/forms/create-form.ts
Comment thread src/forms/bind.ts
Comment thread src/forms/ssr.ts
Comment thread docs/guide/forms.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread src/component/types.ts
Comment thread src/component/index.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 35 changed files in this pull request and generated 8 comments.

Comment thread src/forms/bind.ts Outdated
Comment thread src/forms/ssr.ts Outdated
Comment thread src/component/events.ts Outdated
Comment thread src/forms/create-form.ts
Comment thread src/component/types.ts Outdated
Comment thread src/component/events.ts
Comment thread src/forms/use-field.ts
Comment thread src/component/events.ts
@JosunLP
JosunLP requested a review from Copilot May 20, 2026 06:51
Copilot AI requested review from Copilot and removed request for Copilot May 20, 2026 10:05
Copilot AI requested review from Copilot and removed request for Copilot May 20, 2026 10:06
Copilot AI requested review from Copilot and removed request for Copilot May 20, 2026 10:08
Copilot AI requested review from Copilot and removed request for Copilot May 20, 2026 10:09
Copilot AI requested review from Copilot and removed request for Copilot May 20, 2026 10:10
@JosunLP
JosunLP requested a review from Copilot May 20, 2026 10:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 5 comments.

Comment thread src/forms/types.ts Outdated
Comment thread src/forms/composables.ts
Comment thread src/component/slots.ts
Comment thread src/component/async.ts
Comment thread src/component/inject.ts
Copilot AI requested review from Copilot and removed request for Copilot May 20, 2026 10:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

docs/guide/components.md:501

  • updated() references state.items, but updated hooks don’t have state in scope here. Use the component instance to read state (e.g. this.getState('items')) or otherwise make the desired key list available inside updated().
updated() {
  reconcileKeyed(
    this.shadowRoot!.querySelector('ul')!,
    state.items.map((item) => item.id)
  );

Comment thread src/component/keyed-list.ts Outdated
Comment thread docs/guide/forms.md
Comment thread docs/guide/components.md Outdated
Comment thread src/forms/create-form.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Changes to the build and meta files component Changes to the component module docs Changes to the documentation forms Changes to the forms module github Changes to the github meta data files tests Chenges to the tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants