@dmitryvim/form-builder 0.6.4 → 0.7.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.
@@ -1,17 +1,29 @@
1
1
  import type { State } from "../../types/state.js";
2
+ import type { ValidityScope } from "../../utils/styles.js";
2
3
  /**
3
4
  * Create a base square tile element. Sizing comes from CSS — `.fb-tile` and
4
5
  * its descendants resolve aspect-ratio/dimensions via the theme variables.
5
6
  */
6
7
  export declare function createFileTile(): HTMLElement;
7
8
  /**
8
- * Show an inline error message below the nearest [data-files-wrapper] ancestor.
9
+ * Who raised a file field's message. "action": an upload or library pick;
10
+ * "limit": the render-owned over-limit notice; "validation": a validation
11
+ * pass.
9
12
  */
10
- export declare function showFileError(container: HTMLElement, message: string): void;
13
+ export type FileErrorKind = "action" | "limit" | "validation";
11
14
  /**
12
- * Remove any inline file error message below the nearest [data-files-wrapper] ancestor.
15
+ * Show an inline error message below the nearest [data-files-wrapper]
16
+ * ancestor. Only an "action" message — the direct result of the user's
17
+ * upload or pick — is announced as an alert.
13
18
  */
14
- export declare function clearFileError(container: HTMLElement): void;
19
+ export declare function showFileError(container: HTMLElement, message: string, state: State, kind?: FileErrorKind): void;
20
+ /**
21
+ * Remove the inline file error message of `kind` below the nearest
22
+ * [data-files-wrapper] ancestor; a message of another kind stays.
23
+ */
24
+ export declare function clearFileError(container: HTMLElement, kind?: FileErrorKind): void;
25
+ /** Validation's write into the slot, under the shared mark decision. */
26
+ export declare function markFileValidity(wrapper: HTMLElement, message: string | null, scope: ValidityScope): void;
15
27
  /**
16
28
  * Render (or replace) a delete-overlay with a centred "Delete" button on a tile.
17
29
  * Used by the single-file edit-mode path.
@@ -6,7 +6,6 @@ export interface RenderPillsOptions {
6
6
  state: State;
7
7
  onRemove: ((rid: string) => void) | null;
8
8
  hint?: string;
9
- countInfo?: string;
10
9
  maxCount?: number;
11
10
  isReadonly?: boolean;
12
11
  onLibraryPick?: (() => void) | null;
@@ -15,7 +14,9 @@ export interface RenderPillsOptions {
15
14
  }
16
15
  export declare function renderResourcePills(opts: RenderPillsOptions): void;
17
16
  export declare function renderFileElementEdit(element: FileElement, ctx: RenderContext, wrapper: HTMLElement, pathKey: string): void;
18
- /** Legacy `files` element — multi-file with no upper bound. */
19
- export declare function renderFilesElementEdit(element: FilesElement, ctx: RenderContext, wrapper: HTMLElement, pathKey: string): void;
20
- /** `file` element with `multiple: true` — multi-file honouring maxCount. */
21
- export declare function renderMultipleFileElementEdit(element: FileElement, ctx: RenderContext, wrapper: HTMLElement, pathKey: string): void;
17
+ /**
18
+ * Multi-file edit-mode renderer for both the legacy `files` element and the
19
+ * `file` type with `multiple: true` — they behave identically, `maxCount`
20
+ * included.
21
+ */
22
+ export declare function renderMultiFileElementEdit(element: FileElement | FilesElement, ctx: RenderContext, wrapper: HTMLElement, pathKey: string): void;
@@ -126,15 +126,43 @@ export declare class FormBuilderInstance {
126
126
  */
127
127
  renderForm(root: HTMLElement, schema: Schema, prefill?: Record<string, any>, actions?: ExternalAction[]): void;
128
128
  /**
129
- * Validate form and extract data
130
- * This is a complete copy of the validateForm logic from form-builder.ts
131
- * but uses instance state instead of global state
129
+ * Validate the form and extract its data. `skipValidation` is the draft
130
+ * contract of saveDraft() and the onChange payload: marks are only
131
+ * refreshed or cleared, and the result reports `valid: true, errors: []`.
132
132
  */
133
133
  validateForm(skipValidation?: boolean): FormDataResult;
134
134
  /**
135
- * Get form data
135
+ * Run every rule and return the real result. `marks` decides only what is
136
+ * painted: "full" raises and clears marks and records reported fields;
137
+ * "draft" refreshes or clears marks of reported fields and raises none
138
+ * (see ValidityScope in utils/styles.ts).
139
+ */
140
+ private runValidation;
141
+ /**
142
+ * Read the form: every rule runs and the result is the real
143
+ * `{valid, errors, data}`. Safe to poll — it never paints a new error
144
+ * mark (so a pristine form never turns red), it only refreshes or clears
145
+ * marks that showErrors()/submitForm() drew, and a repeated call on
146
+ * unchanged state touches no DOM at all.
136
147
  */
137
148
  getFormData(): FormDataResult;
149
+ /**
150
+ * Paint every validation error next to its field (and clear marks of
151
+ * fields that are now valid), then return the same result as
152
+ * getFormData(). Call it when the user asks to submit, followed by
153
+ * focusFirstError() to take them to the first problem.
154
+ */
155
+ showErrors(): FormDataResult;
156
+ /**
157
+ * Focus the first field marked invalid, in DOM order, and scroll it into
158
+ * view. A marked group (container, multi-value field, file field) gets
159
+ * focus on its first focusable control, else on the group itself.
160
+ * Does not validate: marks are drawn by showErrors() (or submitForm()),
161
+ * so call showErrors() first. Fields hidden by enableIf are skipped, as is
162
+ * any field that cannot take focus (e.g. inside a hidden slide).
163
+ * @returns true only when focus actually landed on an invalid field
164
+ */
165
+ focusFirstError(): boolean;
138
166
  /**
139
167
  * Submit form with validation
140
168
  */
@@ -13,8 +13,18 @@ export interface ComponentContext {
13
13
  instance: FormBuilderInstance;
14
14
  /** Path prefix for nested fields */
15
15
  path: string;
16
- /** Skip validation checks (for draft mode) */
17
- skipValidation?: boolean;
16
+ /**
17
+ * Draft marking (getFormData, debounced onChange, saveDraft): every rule
18
+ * runs, but only fields a full pass (showErrors, submitForm) reported may
19
+ * be marked. Affects painting only, never the returned errors. See
20
+ * ValidityScope in utils/styles.ts.
21
+ */
22
+ draftMarks?: boolean;
23
+ /**
24
+ * The element being validated is readonly (itself, form-wide, or through a
25
+ * readonly ancestor container): its errors are reported, never marked.
26
+ */
27
+ readonly?: boolean;
18
28
  /**
19
29
  * Recursive element validator, threaded per validateForm pass by
20
30
  * FormBuilderInstance. Containers use it to validate children with the
@@ -25,6 +35,7 @@ export interface ComponentContext {
25
35
  */
26
36
  validateElement?: (element: Element, ctx: {
27
37
  path: string;
38
+ inheritedReadonly?: boolean;
28
39
  }, customScopeRoot?: HTMLElement | null) => {
29
40
  value: any;
30
41
  spread: boolean;
@@ -1,6 +1,16 @@
1
1
  import type { Schema, ExternalAction, Element } from "./schema.js";
2
2
  import type { Config, ResourceMetadata } from "./config.js";
3
3
  export interface State {
4
+ /** `inst-{timestamp}-{random}`; prefixes ids of nodes the instance creates. */
5
+ instanceId: string;
6
+ /** Sequence for instance-unique ids of nodes the instance creates. */
7
+ domIdCounter: number;
8
+ /**
9
+ * Marked elements a full validation pass reported invalid. Draft passes
10
+ * (debounced onChange, saveDraft) may only raise marks for these — see
11
+ * ValidityScope in utils/styles.ts.
12
+ */
13
+ reportedInvalid: WeakSet<HTMLElement>;
4
14
  schema: Schema | null;
5
15
  formRoot: HTMLElement | null;
6
16
  resourceIndex: Map<string, ResourceMetadata>;
@@ -13,6 +23,13 @@ export interface State {
13
23
  syntheticElementIds: WeakMap<Element, string>;
14
24
  /** Counter for generating unique synthetic IDs. */
15
25
  syntheticElementIdCounter: number;
26
+ /**
27
+ * Replaces the file list of an edit-mode multi-file field, keyed by its
28
+ * [data-files-wrapper] element. setFormData/updateField must go through the
29
+ * field's own list — writing only the DOM attribute was reverted by the next
30
+ * add/remove, which re-serializes that list.
31
+ */
32
+ multiFileSetters: WeakMap<HTMLElement, (resourceIds: string[]) => void>;
16
33
  /**
17
34
  * Pending MutationObservers from setupEnableIfListeners that are waiting for a
18
35
  * dependency input to appear in the DOM. They must be disconnected on destroy()
@@ -1,18 +1,79 @@
1
1
  import type { State } from "../types/index.js";
2
2
  /**
3
- * Mark an input's validity: toggle the `invalid` class/title and create,
3
+ * What a validation pass may change on screen. A full pass (showErrors,
4
+ * submitForm) raises and clears marks and records each field it reported
5
+ * invalid. A draft pass (`draftMarks`: getFormData, the debounced onChange,
6
+ * saveDraft) raises, refreshes or clears marks only for fields a full pass
7
+ * reported — polling or typing must not light up fields the user has not
8
+ * submitted — and still clears any mark whose field became valid. Every
9
+ * write below is skipped when the DOM already holds the value, so a
10
+ * repeated pass on unchanged state produces no mutation records (hosts
11
+ * poll getFormData() from MutationObservers). A readonly field
12
+ * (`readonly`) is never marked: the user could not fix it. A field that
13
+ * enableIf hides and shows again is re-rendered as a new element and so
14
+ * comes back unmarked until the next full pass — intended: it reappears
15
+ * clean.
16
+ * ComponentContext satisfies this shape.
17
+ */
18
+ export interface ValidityScope {
19
+ state: State;
20
+ draftMarks?: boolean;
21
+ readonly?: boolean;
22
+ }
23
+ /**
24
+ * The single mark decision shared by input, field and file marks: the
25
+ * message to draw, null to clear, or undefined to leave `target` as it is.
26
+ * Reported fields are tracked by their marked element rather than by path:
27
+ * chip rows renumber their names on removal, which would hand a path-keyed
28
+ * flag to a neighbouring row.
29
+ */
30
+ export declare function resolveMark(target: HTMLElement, message: string | null, scope: ValidityScope): string | null | undefined;
31
+ /** Several rule messages of one field, shown as one line. */
32
+ export declare function joinErrorMessages(messages: string[]): string | null;
33
+ /**
34
+ * Error node shared by input, field and file marks. The id is
35
+ * instance-prefixed so aria-describedby cannot resolve into another form on
36
+ * the page, even one loaded from a second copy of the bundle. No live-region
37
+ * role: a validation message is reached through aria-describedby, and only
38
+ * the direct result of a user action is announced (see showFileError).
39
+ */
40
+ export declare function createErrorNode(state: State, className: string): HTMLElement;
41
+ /** setAttribute, skipped when unchanged (a same-value write still notifies observers). */
42
+ export declare function setAttr(el: Element, name: string, value: string): void;
43
+ /** Point `target` at `node` through aria-describedby, once. */
44
+ export declare function linkDescription(target: HTMLElement, node: HTMLElement): void;
45
+ /** Flag `target` invalid and point it at `node` (null = invalid without text). */
46
+ export declare function setInvalidMark(target: HTMLElement, node: HTMLElement | null, state: State): void;
47
+ /**
48
+ * Drop the invalid state (aria-invalid and what exposeAsGroup added) but
49
+ * keep any description link — for a notice that is not a field state.
50
+ */
51
+ export declare function unsetInvalidState(target: HTMLElement): void;
52
+ /** Undo setInvalidMark and remove `node`; attributes a host set stay. */
53
+ export declare function clearInvalidMark(target: HTMLElement, node: HTMLElement | null): void;
54
+ /**
55
+ * Mark an input's validity: toggle `invalid`/title/aria-invalid and create,
4
56
  * update or remove the paired `.error-message` node. Shared by every scalar
5
57
  * component's validator — this used to be six near-identical copies, each
6
58
  * with its own `error-${Math.random()}` fallback id.
7
59
  */
8
- export declare function markFieldValidity(input: HTMLElement | null, errorMessage: string | null): void;
60
+ export declare function markFieldValidity(input: HTMLElement | null, errorMessage: string | null, scope: ValidityScope): void;
9
61
  /**
10
- * Strip the `.error-message` node paired with an input. Called from
11
- * updateField paths so a programmatic update to a valid value clears any
12
- * stale error surface left over from the previous validation pass —
13
- * otherwise the field stays visibly "invalid" until the next validate cycle.
62
+ * Clear an input's mark entirely. Called from updateField paths so a
63
+ * programmatic update to a valid value clears any stale error surface left
64
+ * over from the previous validation pass — otherwise the field stays
65
+ * visibly "invalid" until the next validate cycle.
14
66
  */
15
67
  export declare function clearFieldError(input: HTMLElement): void;
68
+ /**
69
+ * Field-level counterpart of markFieldValidity for rules that belong to the
70
+ * field as a whole — item counts of multi-value fields and containers, table
71
+ * and richinput rules. The mark sits on the element whose `data-field-path`
72
+ * is `fieldPath` — the `.fb-field-wrapper` — with its message as the
73
+ * wrapper's last child. A field hidden by enableIf (its wrapper, or the
74
+ * placeholder that replaced it) is not marked.
75
+ */
76
+ export declare function markFieldGroupValidity(scopeRoot: HTMLElement, fieldPath: string, errorMessage: string | null, scope: ValidityScope): void;
16
77
  /**
17
78
  * Shared trash-can icon used by every per-item remove button (multi-text
18
79
  * chips, multi-container items, etc.). Stays as one inline SVG so a single
@@ -28,6 +89,7 @@ export declare const BIN_ICON_SVG = "<svg width=\"14\" height=\"14\" viewBox=\"0
28
89
  * - `[data-fb-slide-card]` — items inside `container.displayMode:"slides"`
29
90
  * - `[data-fb-label-row]` — every standard field-label container
30
91
  * - `.fb-prefill-hint` — schema-driven prefill suggestion pills
92
+ * - `[aria-invalid]` — controls marked invalid by validation
31
93
  *
32
94
  * Idempotent via a flag attribute on the document head.
33
95
  */
@@ -1,3 +1,17 @@
1
+ import type { ComponentContext, Element } from "../types/index.js";
1
2
  import type { State } from "../types/state.js";
3
+ /**
4
+ * required / minCount / maxCount over a field's `count` items. The message
5
+ * keys differ per field family: files say "files", everything else "items".
6
+ */
7
+ export declare function countRuleMessages(element: Element, count: number, state: State, keys?: {
8
+ min: string;
9
+ max: string;
10
+ }): string[];
11
+ /**
12
+ * Count rules of a multi-value field or container over its `filledCount`
13
+ * items: pushes "path: message" errors and marks the field with all of them.
14
+ */
15
+ export declare function validateItemCount(element: Element, key: string, filledCount: number, context: ComponentContext, errors: string[]): void;
2
16
  export declare function makeFieldHint(element: any, state: State): string;
3
17
  export declare function validateSchema(schema: any): string[];
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.4",
6
+ "version": "0.7.0",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "./dist/cjs/index.cjs",
9
9
  "module": "./dist/esm/index.js",