declarative-forms 2.0.0 โ†’ 2.1.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.
package/README.md CHANGED
@@ -1,102 +1,110 @@
1
1
  # declarative-forms
2
2
 
3
- Declarative forms for the web that make front-end development less front-end development.
3
+ **Ask the user for an object, the way `prompt()` asks for a string.**
4
4
 
5
- Whenever you need input from a user, you describe the data you need and the rules around it. `declarative-forms` takes care of the front-end work to ask for that information: rendering the fields, wiring updates, loading async option sets, managing nested structures, handling modal stacks, tabs, and keeping the UI in sync as values change.
5
+ Every other form library gives you a _component to mount_. This one gives you a
6
+ _function to call_. There is no component, no form state, no mount point, and no
7
+ place in your tree where the form has to live. You ask a question from wherever
8
+ you happen to be standing in your code, and you get the answer back.
6
9
 
7
- ๐Ÿ“– **[Read the documentation](https://wolfoo2931.github.io/declarative-forms/)**
10
+ ```ts
11
+ import { ask } from 'declarative-forms';
12
+
13
+ const release = await ask([
14
+ { name: 'title', displayName: 'Release title' },
15
+ { name: 'notes', kind: 'textarea', displayName: 'What changed' },
16
+ {
17
+ name: 'reviewers',
18
+ kind: 'select',
19
+ multiple: true,
20
+ displayName: 'Sign-off from',
21
+ options: () => fetchReviewers(),
22
+ },
23
+ ]);
8
24
 
9
- You do not have to build the front-end plumbing by hand just to collect structured input. Instead, you provide a schema-like description of the form and the library handles the rest.
25
+ // { title: 'Sunrise 2.0', notes: 'โ€ฆ', reviewers: ['Ada', 'Grace'] }
26
+ ```
10
27
 
11
- This is especially useful for complex forms, complex data structures, and workflows where the interface is mostly there to collect and validate user input rather than to render bespoke product UI.
28
+ That is the whole integration. No `<Form>`, no `useForm`, no `onChange`, no
29
+ `useState`, no layout, no `<div>`. The dialog draws itself, loads its own
30
+ options, keeps every field in sync, and resolves.
12
31
 
13
- ## What it is
32
+ `ask` ships with the library โ€”
33
+ [what it takes and what it resolves with](https://wolfoo2931.github.io/declarative-forms/guide/asking-for-data#the-ask-function).
14
34
 
15
- At its core, `declarative-forms` is a framework-agnostic form engine.
35
+ ๐Ÿ“– **[Read the documentation](https://wolfoo2931.github.io/declarative-forms/)** ยท
36
+ ๐ŸŽ› **[Try the live demo](https://wolfoo2931.github.io/declarative-forms/#demo)**
16
37
 
17
- You define a form like this:
38
+ ## Why a call and not a component
18
39
 
19
- ```ts
20
- import 'declarative-forms/styles.css';
21
- import { DeclarativeForm } from 'declarative-forms';
40
+ `window.prompt()` is the one form API the browser gives you for free. You ask,
41
+ the browser draws the dialog, you get the answer. No markup, no state, no
42
+ layout, no lifecycle. Its only flaw is that it can ask for exactly one string.
22
43
 
23
- const form = new DeclarativeForm({
24
- fields: [
25
- { name: 'title', displayName: 'Title' },
26
- {
27
- name: 'language',
28
- kind: 'select',
29
- displayName: 'Language',
30
- options: [
31
- { value: 'en', label: 'English' },
32
- { value: 'de', label: 'German' },
33
- ],
34
- defaultValue: 'en',
35
- },
36
- {
37
- name: 'repo',
38
- kind: 'select',
39
- displayName: 'Repository',
40
- reloadOnChangeOf: ['language'],
41
- options: async ({ data }) => {
42
- const lang = String(data['language'] ?? 'en');
43
- return [{ value: `repo-${lang}`, label: `Repo (${lang})` }];
44
- },
45
- },
46
- ],
47
- onConfirm: (values) => console.log(values),
48
- onCancel: () => {},
49
- });
44
+ Everything the industry built to replace it went the other way โ€” into the
45
+ component tree:
50
46
 
51
- form.openInModal();
47
+ ```jsx
48
+ // The component model: the form is a thing that lives somewhere.
49
+ const { register, handleSubmit } = useForm();
50
+ return (
51
+ <Modal open={open} onClose={() => setOpen(false)}>
52
+ <form onSubmit={handleSubmit(onSubmit)}>
53
+ <input {...register('title')} />โ€ฆ
54
+ </form>
55
+ </Modal>
56
+ );
52
57
  ```
53
58
 
54
- This is not a generated UI package or a full application framework. It is a runtime for declarative form behavior: you describe the data model and form rules, and it turns that into a working user flow with the UI concerns handled for you.
55
-
56
- ## What makes it different
57
-
58
- The library is built around a few real strengths that show up in the codebase:
59
+ Now the form has a _location_. It needs a parent, a piece of `open` state, a
60
+ close handler, a submit handler, and a route from the answer back to the code
61
+ that wanted it. For a product's signup page, that is fine โ€” you were going to
62
+ lay that page out anyway. For the two hundredth settings dialog in an internal
63
+ tool, it is all overhead: you did not want a form, you wanted an answer.
59
64
 
60
- - Data-first form definitions instead of manual DOM creation
61
- - Cross-field reactivity via `reloadOnChangeOf`, `isActive`, `defaultValue`, and `onFormChange`
62
- - Async option loading and async button predicates
63
- - Built-in modal stack support for nested dialogs and wizards
64
- - Tabbed forms and array-of-record editing
65
- - Automatic handling of many front-end concerns: layout, state sync, stacking, dialog behavior, and reusable field logic
66
- - Zero runtime dependencies
67
- - Works in plain JavaScript or TypeScript, not just React/Vue/Svelte
65
+ `declarative-forms` keeps `prompt()`'s shape and removes its one-string limit.
66
+ The form is not a thing that lives somewhere. It is a question, asked and
67
+ answered:
68
68
 
69
- The library also includes a few opinionated features that are especially useful for interactive tools:
70
-
71
- - computed fields that recalculate before confirmation
72
- - file fields that require a `persistFile` callback to convert a local file into a stored URL
73
- - custom render fields for embedding arbitrary DOM or widget logic inside a form
74
- - message, cards, and custom field types for dynamic UI without a separate component library
75
-
76
- ## When to use this library
69
+ ```ts
70
+ if (await confirmDetails()) { โ€ฆ }
77
71
 
78
- Use `declarative-forms` when the form is more like an application workflow than a static input form.
72
+ const filters = await ask(filterFields);
73
+ const entry = await ask(entryFields, { confirmLabel: 'Add' });
74
+ ```
79
75
 
80
- It fits especially well for:
76
+ ## What that model gives you that a component cannot
81
77
 
82
- - admin panels and settings dialogs
83
- - configuration flows with conditions and dependencies
84
- - modal-based wizards with step logic
85
- - forms whose options depend on other fields
86
- - array/list editing with nested record forms
87
- - internal tools that need compact, data-driven UI without a UI framework dependency
78
+ **Dialogs stack, and the inner one can read the outer ones.** Because a form is
79
+ a call and not a node, opening one from inside another is just... calling it.
80
+ The library keeps the stack, and any field in any dialog can read the values of
81
+ every dialog beneath it through `stackData` โ€” with no lifted state, no context
82
+ provider, and no prop drilling, because there is no tree to lift through.
88
83
 
89
- ## Core ideas in the codebase
84
+ ```ts
85
+ {
86
+ name: 'recap',
87
+ kind: 'message',
88
+ // stackData[0] is the outermost dialog, still open behind this one.
89
+ message: ({ data, stackData }) =>
90
+ `Publishing ${stackData[0]['title']} ${data['when'] === 'now' ? 'now' : 'later'}.`,
91
+ }
92
+ ```
90
93
 
91
- The library is centered on a few runtime concepts:
94
+ **Lists of records are just the same call again.** An `array` field opens one
95
+ dialog per entry, which is the natural shape for a record, and awkward to do
96
+ inline.
92
97
 
93
- - `DeclarativeForm`: the main runtime that owns rendering, values, tabs, and modal behavior
94
- - `FieldDescriptor`: a plain object describing each field (`name`, `kind`, `options`, `tab`, `isActive`, etc.)
95
- - `FormValues`: the current data dictionary keyed by field name
96
- - `subscribeOnInput`: a way to observe form updates without wiring lots of DOM listeners
97
- - `ButtonDescriptor`: supports async enable/disable logic and โ€œdo not closeโ€ actions for wizards
98
+ **Works anywhere, because it needs nothing.** Plain DOM plus one web component,
99
+ no runtime dependencies. Call it from React, Vue, Svelte, Angular, an Electron
100
+ main-window script, or a `<script type="module">` tag in a static page. It never
101
+ touches your render tree, so there is nothing to integrate.
98
102
 
99
- This is why the project feels more like a small โ€œform engineโ€ than a typical form component library.
103
+ **The descriptor is live, not static.** `options`, `isActive`, `defaultValue`,
104
+ `placeholder`, `message`, `tab` and `compute` may each be a _function of the
105
+ current values_, and `reloadOnChangeOf` declares which field depends on which.
106
+ Options loaded from your API that change when another field changes are the
107
+ core feature, not an extension point.
100
108
 
101
109
  ## Install
102
110
 
@@ -104,96 +112,92 @@ This is why the project feels more like a small โ€œform engineโ€ than a typical
104
112
  npm install declarative-forms
105
113
  ```
106
114
 
107
- And include the default CSS:
108
-
109
115
  ```ts
110
116
  import 'declarative-forms/styles.css';
111
117
  ```
112
118
 
113
- ## Example: modal form
119
+ That is the default look, and it follows the operating system's light/dark
120
+ setting on its own. The v1 look ships as `declarative-forms/classic.css`.
121
+
122
+ No build step is required โ€” the ESM build loads directly in a `<script type="module">`.
123
+
124
+ ## The full API, if you want the object instead of the promise
125
+
126
+ `ask` is a thin wrapper over the object underneath โ€” a normal object you can
127
+ hold on to, embed in a page instead of a dialog, subscribe to, and drive from
128
+ code:
114
129
 
115
130
  ```ts
116
131
  import { DeclarativeForm } from 'declarative-forms';
117
132
 
118
133
  const form = new DeclarativeForm({
119
- fields: [
120
- { name: 'name', displayName: 'Name' },
121
- {
122
- name: 'role',
123
- kind: 'select',
124
- displayName: 'Role',
125
- options: ['Admin', 'Editor', 'Viewer'],
126
- defaultValue: 'Editor',
127
- },
128
- ],
129
- onConfirm: (values) => {
130
- console.log('submitted', values);
131
- },
132
- onCancel: () => {
133
- console.log('cancelled');
134
- },
134
+ fields: [...],
135
+ onConfirm: (values) => save(values),
136
+ onCancel: () => {},
135
137
  });
136
138
 
137
- form.openInModal();
138
- ```
139
-
140
- ## Example: reactive field logic
139
+ form.openInModal(); // โ€ฆor:
140
+ form.appendInElement(document.querySelector('#panel'));
141
141
 
142
- ```ts
143
- const form = new DeclarativeForm({
144
- fields: [
145
- {
146
- name: 'source',
147
- kind: 'select',
148
- displayName: 'Source',
149
- options: ['GitHub', 'GitLab'],
150
- defaultValue: 'GitHub',
151
- },
152
- {
153
- name: 'token',
154
- displayName: 'Token',
155
- type: 'password',
156
- isActive: ({ data }) => data['source'] === 'GitLab',
157
- },
158
- {
159
- name: 'repo',
160
- kind: 'select',
161
- displayName: 'Repository',
162
- reloadOnChangeOf: ['source'],
163
- options: async ({ data }) => {
164
- const source = String(data['source'] ?? 'GitHub');
165
- return [{ value: `${source}-repo`, label: `${source} repo` }];
166
- },
167
- },
168
- ],
169
- onConfirm: (values) => console.log(values),
170
- });
142
+ form.subscribeOnInput((values) => renderPreview(values));
143
+ form.field('role')?.setValue('Admin');
171
144
  ```
172
145
 
173
- This is the sort of behavior the library is designed to make practical without building a lot of custom event wiring.
146
+ See [the API reference](https://wolfoo2931.github.io/declarative-forms/reference/api).
174
147
 
175
- ## Documentation
148
+ ## The ten field kinds
176
149
 
177
- Full documentation is at **[wolfoo2931.github.io/declarative-forms](https://wolfoo2931.github.io/declarative-forms/)**:
150
+ `text` ยท `textarea` ยท `select` (single, multiple, async, searchable) ยท
151
+ `checkbox` ยท `message` ยท `file` ยท `computed` ยท `cards` ยท `custom` ยท `array`
178
152
 
179
- - [Getting started](https://wolfoo2931.github.io/declarative-forms/guide/getting-started)
180
- - [Field kinds](https://wolfoo2931.github.io/declarative-forms/guide/fields/) โ€” the ten built-in kinds
181
- - [Reactivity](https://wolfoo2931.github.io/declarative-forms/guide/reactivity) โ€” `isActive`, `reloadOnChangeOf`, computed fields
182
- - [API reference](https://wolfoo2931.github.io/declarative-forms/reference/api)
183
- - [Migrating from v1](https://wolfoo2931.github.io/declarative-forms/migration-v1)
153
+ Full descriptions in **[Field kinds](https://wolfoo2931.github.io/declarative-forms/guide/fields/)**.
184
154
 
185
- ## Status and limitations
155
+ ## Where it fits
186
156
 
187
- Worth knowing before you adopt it:
157
+ Best in **settings and metadata dialogs for tool-like apps**: many optional
158
+ fields, grouped into tabs, where the available choices depend on what the user
159
+ already picked.
188
160
 
189
- - **Accessibility is a work in progress.** Labels, ids and focusable buttons are correct; dialog semantics, combobox ARIA and keyboard-reachable checkboxes are not there yet. The [accessibility page](https://wolfoo2931.github.io/declarative-forms/accessibility) lists every known gap.
190
- - **There is no validation framework.** `isActive` handles conditional fields and button predicates gate submission, but there is no rule engine or error-message system.
191
- - **The rendered DOM is a frozen contract.** Class names, ids and structure are stable across the v1 โ†’ v2 rewrite, so existing stylesheets keep working. See the [DOM contract](https://wolfoo2931.github.io/declarative-forms/dom-contract).
161
+ - admin panels and settings dialogs
162
+ - configuration flows with conditions and dependencies
163
+ - modal wizards with several steps
164
+ - forms whose options come from a server and depend on other fields
165
+ - editing lists whose entries are records of their own
166
+ - internal tools that need a data-driven UI without a framework dependency
167
+
168
+ ## Where it does not fit
169
+
170
+ Plainly, so you can rule it out fast:
171
+
172
+ - **It is not a general-purpose form library.** It renders one fixed layout. If
173
+ you need control over the markup โ€” a product signup page, a marketing form โ€”
174
+ use a state library and write the markup yourself.
175
+ - **There is no validation framework.** No `required`, no rule objects, no
176
+ schema. There _are_ per-field error, warning and loading messages
177
+ (`setTooltipError` and friends), and `isValidRecord` and a button's `isActive`
178
+ gate submission โ€” but you write each check yourself, and the message and the
179
+ gate are separate things you wire up separately. See
180
+ [Validation](https://wolfoo2931.github.io/declarative-forms/guide/validation).
181
+ - **Accessibility is unfinished.** Labels, ids, focusable buttons and
182
+ keyboard-reachable checkboxes are correct. Dialog semantics, a focus trap, and
183
+ combobox ARIA are not. Read
184
+ [Accessibility](https://wolfoo2931.github.io/declarative-forms/accessibility)
185
+ in full before using it where conformance is required.
186
+ - **The rendered DOM is a frozen contract.** Class names, ids and structure are
187
+ stable across the v1 โ†’ v2 rewrite, so existing stylesheets keep working. See
188
+ the [DOM contract](https://wolfoo2931.github.io/declarative-forms/dom-contract).
192
189
 
193
- ## Summary
190
+ ## Documentation
194
191
 
195
- `declarative-forms` is best thought of as a declarative form runtime for web dialogs and tool-like workflows.
192
+ - [Asking for data](https://wolfoo2931.github.io/declarative-forms/guide/asking-for-data) โ€” the model, and `ask`
193
+ - [Getting started](https://wolfoo2931.github.io/declarative-forms/guide/getting-started)
194
+ - [Field kinds](https://wolfoo2931.github.io/declarative-forms/guide/fields/)
195
+ - [Reactivity](https://wolfoo2931.github.io/declarative-forms/guide/reactivity) โ€” `isActive`, `reloadOnChangeOf`, computed fields
196
+ - [Validation](https://wolfoo2931.github.io/declarative-forms/guide/validation) โ€” checks, tooltip messages, gating submit
197
+ - [Modals & stacking](https://wolfoo2931.github.io/declarative-forms/guide/modals)
198
+ - [API reference](https://wolfoo2931.github.io/declarative-forms/reference/api)
199
+ - [Migrating from v1](https://wolfoo2931.github.io/declarative-forms/migration-v1)
196
200
 
197
- It is for the cases where the main job is not โ€œbuild another UI component,โ€ but โ€œask the user for structured data.โ€ You define what needs to be collected, and the library handles the front-end work to gather it: field rendering, behavior, defaults, dynamic changes, nested records, modal stacking, and workflow handling.
201
+ ## License
198
202
 
199
- That is the core difference: instead of writing front-end code for every form interaction, you describe the data and rules, and the runtime takes care of the responsibilities that usually consume most of the work in form-heavy interfaces.
203
+ MIT ยฉ Oliver Wolf
package/dist/index.cjs CHANGED
@@ -195,6 +195,21 @@ var Field = class {
195
195
  );
196
196
  if (value !== void 0) this.setValue(value);
197
197
  }
198
+ /**
199
+ * Apply a plainly declared default value straight away.
200
+ *
201
+ * {@link applyDefaultValue} is awaited during initialisation, so a value it
202
+ * applies cannot be seen by the first layout pass. A default that is neither a
203
+ * function nor a promise needs no awaiting, so it is applied at build time and
204
+ * `isActive` predicates reading it are right from the first paint. The async
205
+ * pass runs afterwards regardless and simply sets the same value again.
206
+ */
207
+ applyStaticDefaultValue() {
208
+ const value = this.descriptor.defaultValue;
209
+ if (value === void 0 || typeof value === "function") return;
210
+ if (value instanceof Promise) return;
211
+ this.setValue(value);
212
+ }
198
213
  /**
199
214
  * Hook for setup that cannot happen in {@link build} โ€” loading select
200
215
  * options, or a first render that needs the form context. May be synchronous.
@@ -2106,6 +2121,7 @@ var DeclarativeForm = class _DeclarativeForm {
2106
2121
  closeWith: (action) => this.close(action)
2107
2122
  });
2108
2123
  this.buildFields();
2124
+ this.applyInitialLayout();
2109
2125
  this.ready = this.initialize();
2110
2126
  }
2111
2127
  // ------------------------------------------------------------------ setup
@@ -2127,12 +2143,40 @@ var DeclarativeForm = class _DeclarativeForm {
2127
2143
  );
2128
2144
  }
2129
2145
  const field = this.registry.create(descriptor, context);
2146
+ field.applyStaticDefaultValue();
2130
2147
  this.fieldList.push(field);
2131
2148
  this.fieldsByName.set(descriptor.name, field);
2132
2149
  this.tooltips.register(descriptor.name, field.tooltipMarker);
2133
2150
  this.formElement.appendChild(field.wrapper);
2134
2151
  }
2135
2152
  }
2153
+ /**
2154
+ * Decide field visibility and build the tab strip synchronously, before the
2155
+ * caller gets the form back.
2156
+ *
2157
+ * {@link initialize} is async โ€” it awaits option loads and default values โ€”
2158
+ * but `openInModal()` normally runs in the same tick as the constructor.
2159
+ * Without this pass the dialog paints every field of every tab, then collapses
2160
+ * to the active tab once that first async update lands, which is a visible
2161
+ * layout shift as soon as any field loads its options over the network.
2162
+ *
2163
+ * Only visibility is decided here: no `onFormChange`, no `onFormUpdate`, and
2164
+ * the value model is left untouched, so the regular update cycle still sees
2165
+ * the first real change and refreshes the buttons.
2166
+ */
2167
+ applyInitialLayout() {
2168
+ const values = this.getValues();
2169
+ const activeTabs = /* @__PURE__ */ new Set();
2170
+ for (const field of this.fieldList) {
2171
+ const active = field.descriptor.isActive?.({
2172
+ ...this.contextFor(field.descriptor),
2173
+ data: values
2174
+ }) ?? true;
2175
+ field.setActive(active);
2176
+ if (active) for (const tab of field.tabs()) activeTabs.add(tab);
2177
+ }
2178
+ this.renderTabs(activeTabs);
2179
+ }
2136
2180
  async initialize() {
2137
2181
  await Promise.allSettled(
2138
2182
  this.fieldList.map(
@@ -2470,6 +2514,44 @@ var DeclarativeForm = class _DeclarativeForm {
2470
2514
  }
2471
2515
  };
2472
2516
 
2517
+ // src/core/ask.ts
2518
+ function ask(fields, options = {}) {
2519
+ return askWith(DeclarativeForm, fields, options);
2520
+ }
2521
+ function askWith(FormClass, fields, options = {}) {
2522
+ const { modal, dismissable = true, buttons, ...formOptions } = options;
2523
+ return new Promise((resolve) => {
2524
+ let settled = false;
2525
+ const settle = (values) => {
2526
+ if (settled) return;
2527
+ settled = true;
2528
+ setTimeout(() => resolve(values), 0);
2529
+ };
2530
+ new FormClass({
2531
+ ...formOptions,
2532
+ fields,
2533
+ buttons: buttons && settleOnClose(buttons, settle),
2534
+ onConfirm: (values) => settle(values),
2535
+ onCancel: dismissable ? () => settle(void 0) : void 0
2536
+ }).openInModal(modal);
2537
+ });
2538
+ }
2539
+ function settleOnClose(buttons, settle) {
2540
+ return Object.fromEntries(
2541
+ Object.entries(buttons).map(([label, descriptor]) => [
2542
+ label,
2543
+ descriptor.doNotCloseModal ? descriptor : {
2544
+ ...descriptor,
2545
+ action: async (values) => {
2546
+ const result = await descriptor.action?.(values);
2547
+ settle(values);
2548
+ return result;
2549
+ }
2550
+ }
2551
+ ])
2552
+ );
2553
+ }
2554
+
2473
2555
  exports.ArrayField = ArrayField;
2474
2556
  exports.ButtonBar = ButtonBar;
2475
2557
  exports.CardsField = CardsField;
@@ -2496,6 +2578,7 @@ exports.TextAreaField = TextAreaField;
2496
2578
  exports.TextField = TextField;
2497
2579
  exports.TooltipController = TooltipController;
2498
2580
  exports.UpdateScheduler = UpdateScheduler;
2581
+ exports.ask = ask;
2499
2582
  exports.deepEqual = deepEqual;
2500
2583
  exports.defaultFieldRegistry = defaultFieldRegistry;
2501
2584
  exports.defineDlSelect = defineDlSelect;