declarative-forms 2.0.1 โ†’ 2.1.1

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,118 @@
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:
69
+ ```ts
70
+ if (await confirmDetails()) { โ€ฆ }
70
71
 
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
72
+ const filters = await ask(filterFields);
73
+ const entry = await ask(entryFields, { confirmLabel: 'Add' });
74
+ ```
75
75
 
76
- ## When to use this library
76
+ ## What that model gives you that a component cannot
77
77
 
78
- Use `declarative-forms` when the form is more like an application workflow than a static input form.
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.
79
83
 
80
- It fits especially well for:
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
+ ```
81
93
 
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
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.
88
97
 
89
- ## Core ideas in the codebase
98
+ **Editing is the same call with the object in it.** `prompt()` takes a starting
99
+ value; so does this. One field list serves both directions โ€” extra keys on the
100
+ object you pass are ignored, so the record you already have goes in as-is.
90
101
 
91
- The library is centered on a few runtime concepts:
102
+ ```ts
103
+ const updated = await ask(userFields, { defaultValues: user });
104
+ ```
92
105
 
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
106
+ **Works anywhere, because it needs nothing.** Plain DOM plus one web component,
107
+ no runtime dependencies. Call it from React, Vue, Svelte, Angular, an Electron
108
+ main-window script, or a `<script type="module">` tag in a static page. It never
109
+ touches your render tree, so there is nothing to integrate.
98
110
 
99
- This is why the project feels more like a small โ€œform engineโ€ than a typical form component library.
111
+ **The descriptor is live, not static.** `options`, `isActive`, `defaultValue`,
112
+ `placeholder`, `message`, `tab` and `compute` may each be a _function of the
113
+ current values_, and `reloadOnChangeOf` declares which field depends on which.
114
+ Options loaded from your API that change when another field changes are the
115
+ core feature, not an extension point.
100
116
 
101
117
  ## Install
102
118
 
@@ -104,101 +120,92 @@ This is why the project feels more like a small โ€œform engineโ€ than a typical
104
120
  npm install declarative-forms
105
121
  ```
106
122
 
107
- And include the stylesheet:
108
-
109
123
  ```ts
110
124
  import 'declarative-forms/styles.css';
111
125
  ```
112
126
 
113
- That is the default look โ€” a modern theme that follows the operating system's
114
- light/dark setting. The v1 look is still shipped as
115
- `declarative-forms/classic.css` for installations with overrides written
116
- against it.
127
+ That is the default look, and it follows the operating system's light/dark
128
+ setting on its own. The v1 look ships as `declarative-forms/classic.css`.
129
+
130
+ No build step is required โ€” the ESM build loads directly in a `<script type="module">`.
117
131
 
118
- ## Example: modal form
132
+ ## The full API, if you want the object instead of the promise
133
+
134
+ `ask` is a thin wrapper over the object underneath โ€” a normal object you can
135
+ hold on to, embed in a page instead of a dialog, subscribe to, and drive from
136
+ code:
119
137
 
120
138
  ```ts
121
139
  import { DeclarativeForm } from 'declarative-forms';
122
140
 
123
141
  const form = new DeclarativeForm({
124
- fields: [
125
- { name: 'name', displayName: 'Name' },
126
- {
127
- name: 'role',
128
- kind: 'select',
129
- displayName: 'Role',
130
- options: ['Admin', 'Editor', 'Viewer'],
131
- defaultValue: 'Editor',
132
- },
133
- ],
134
- onConfirm: (values) => {
135
- console.log('submitted', values);
136
- },
137
- onCancel: () => {
138
- console.log('cancelled');
139
- },
142
+ fields: [...],
143
+ onConfirm: (values) => save(values),
144
+ onCancel: () => {},
140
145
  });
141
146
 
142
- form.openInModal();
143
- ```
144
-
145
- ## Example: reactive field logic
147
+ form.openInModal(); // โ€ฆor:
148
+ form.appendInElement(document.querySelector('#panel'));
146
149
 
147
- ```ts
148
- const form = new DeclarativeForm({
149
- fields: [
150
- {
151
- name: 'source',
152
- kind: 'select',
153
- displayName: 'Source',
154
- options: ['GitHub', 'GitLab'],
155
- defaultValue: 'GitHub',
156
- },
157
- {
158
- name: 'token',
159
- displayName: 'Token',
160
- type: 'password',
161
- isActive: ({ data }) => data['source'] === 'GitLab',
162
- },
163
- {
164
- name: 'repo',
165
- kind: 'select',
166
- displayName: 'Repository',
167
- reloadOnChangeOf: ['source'],
168
- options: async ({ data }) => {
169
- const source = String(data['source'] ?? 'GitHub');
170
- return [{ value: `${source}-repo`, label: `${source} repo` }];
171
- },
172
- },
173
- ],
174
- onConfirm: (values) => console.log(values),
175
- });
150
+ form.subscribeOnInput((values) => renderPreview(values));
151
+ form.field('role')?.setValue('Admin');
176
152
  ```
177
153
 
178
- This is the sort of behavior the library is designed to make practical without building a lot of custom event wiring.
154
+ See [the API reference](https://wolfoo2931.github.io/declarative-forms/reference/api).
179
155
 
180
- ## Documentation
156
+ ## The ten field kinds
181
157
 
182
- Full documentation is at **[wolfoo2931.github.io/declarative-forms](https://wolfoo2931.github.io/declarative-forms/)**:
158
+ `text` ยท `textarea` ยท `select` (single, multiple, async, searchable) ยท
159
+ `checkbox` ยท `message` ยท `file` ยท `computed` ยท `cards` ยท `custom` ยท `array`
183
160
 
184
- - [Getting started](https://wolfoo2931.github.io/declarative-forms/guide/getting-started)
185
- - [Field kinds](https://wolfoo2931.github.io/declarative-forms/guide/fields/) โ€” the ten built-in kinds
186
- - [Reactivity](https://wolfoo2931.github.io/declarative-forms/guide/reactivity) โ€” `isActive`, `reloadOnChangeOf`, computed fields
187
- - [API reference](https://wolfoo2931.github.io/declarative-forms/reference/api)
188
- - [Migrating from v1](https://wolfoo2931.github.io/declarative-forms/migration-v1)
161
+ Full descriptions in **[Field kinds](https://wolfoo2931.github.io/declarative-forms/guide/fields/)**.
189
162
 
190
- ## Status and limitations
163
+ ## Where it fits
191
164
 
192
- Worth knowing before you adopt it:
165
+ Best in **settings and metadata dialogs for tool-like apps**: many optional
166
+ fields, grouped into tabs, where the available choices depend on what the user
167
+ already picked.
193
168
 
194
- - **Accessibility is a work in progress.** Labels, ids, focusable buttons and โ€” under the default stylesheet โ€” keyboard-reachable checkboxes are correct; dialog semantics and combobox ARIA are not there yet. The [accessibility page](https://wolfoo2931.github.io/declarative-forms/accessibility) lists every known gap.
195
- - **There is no validation framework.** `isActive` handles conditional fields and button predicates gate submission, but there is no rule engine or error-message system.
196
- - **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).
169
+ - admin panels and settings dialogs
170
+ - configuration flows with conditions and dependencies
171
+ - modal wizards with several steps
172
+ - forms whose options come from a server and depend on other fields
173
+ - editing lists whose entries are records of their own
174
+ - internal tools that need a data-driven UI without a framework dependency
175
+
176
+ ## Where it does not fit
177
+
178
+ Plainly, so you can rule it out fast:
179
+
180
+ - **It is not a general-purpose form library.** It renders one fixed layout. If
181
+ you need control over the markup โ€” a product signup page, a marketing form โ€”
182
+ use a state library and write the markup yourself.
183
+ - **There is no validation framework.** No `required`, no rule objects, no
184
+ schema. There _are_ per-field error, warning and loading messages
185
+ (`setTooltipError` and friends), and `isValidRecord` and a button's `isActive`
186
+ gate submission โ€” but you write each check yourself, and the message and the
187
+ gate are separate things you wire up separately. See
188
+ [Validation](https://wolfoo2931.github.io/declarative-forms/guide/validation).
189
+ - **Accessibility is unfinished.** Labels, ids, focusable buttons and
190
+ keyboard-reachable checkboxes are correct. Dialog semantics, a focus trap, and
191
+ combobox ARIA are not. Read
192
+ [Accessibility](https://wolfoo2931.github.io/declarative-forms/accessibility)
193
+ in full before using it where conformance is required.
194
+ - **The rendered DOM is a frozen contract.** Class names, ids and structure are
195
+ stable across the v1 โ†’ v2 rewrite, so existing stylesheets keep working. See
196
+ the [DOM contract](https://wolfoo2931.github.io/declarative-forms/dom-contract).
197
197
 
198
- ## Summary
198
+ ## Documentation
199
199
 
200
- `declarative-forms` is best thought of as a declarative form runtime for web dialogs and tool-like workflows.
200
+ - [Asking for data](https://wolfoo2931.github.io/declarative-forms/guide/asking-for-data) โ€” the model, and `ask`
201
+ - [Getting started](https://wolfoo2931.github.io/declarative-forms/guide/getting-started)
202
+ - [Field kinds](https://wolfoo2931.github.io/declarative-forms/guide/fields/)
203
+ - [Reactivity](https://wolfoo2931.github.io/declarative-forms/guide/reactivity) โ€” `isActive`, `reloadOnChangeOf`, computed fields
204
+ - [Validation](https://wolfoo2931.github.io/declarative-forms/guide/validation) โ€” checks, tooltip messages, gating submit
205
+ - [Modals & stacking](https://wolfoo2931.github.io/declarative-forms/guide/modals)
206
+ - [API reference](https://wolfoo2931.github.io/declarative-forms/reference/api)
207
+ - [Migrating from v1](https://wolfoo2931.github.io/declarative-forms/migration-v1)
201
208
 
202
- 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.
209
+ ## License
203
210
 
204
- 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.
211
+ 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.
@@ -929,8 +944,6 @@ function injectStyles() {
929
944
  // src/components/DlSelect.ts
930
945
  var SVG_NS = "http://www.w3.org/2000/svg";
931
946
  var ARROW_PATH = "M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z";
932
- var MIN_LOADING_MS = 1500;
933
- var LOADING_DELAY_MS = 100;
934
947
  var CustomElementBase = typeof HTMLElement === "undefined" ? class {
935
948
  } : HTMLElement;
936
949
  var DlOption = class extends CustomElementBase {
@@ -943,7 +956,23 @@ var DlOption = class extends CustomElementBase {
943
956
  return this.getAttribute("displayWhenSelected") ?? this.innerText;
944
957
  }
945
958
  };
946
- var DlSelect = class extends CustomElementBase {
959
+ var DlSelect = class _DlSelect extends CustomElementBase {
960
+ /**
961
+ * How long a load may run before the skeleton is shown at all. A load that
962
+ * finishes inside this window never flashes one.
963
+ */
964
+ static showLoadingAfterMs = 100;
965
+ /**
966
+ * Once the skeleton *has* been shown, it is held for at least this long, so
967
+ * it cannot appear and vanish in the same instant.
968
+ *
969
+ * Both are statics so a test suite can set them to 0. They are deliberate
970
+ * pauses for human eyes; to a test they are pure latency, and they are why
971
+ * suites end up sleeping around these selects instead of waiting on a
972
+ * condition. Wait on `isLoading` rather than the `dl-select-loading` class:
973
+ * `isLoading` flips synchronously, the class lags by `showLoadingAfterMs`.
974
+ */
975
+ static minLoadingMs = 1500;
947
976
  optionsWrapper;
948
977
  inputWrapper;
949
978
  noMatchesHint;
@@ -1052,14 +1081,14 @@ var DlSelect = class extends CustomElementBase {
1052
1081
  this.classList.add("dl-select-loading");
1053
1082
  this.optionsWrapper.remove();
1054
1083
  this.loadingStartedAt = Date.now();
1055
- }, LOADING_DELAY_MS)
1084
+ }, _DlSelect.showLoadingAfterMs)
1056
1085
  );
1057
1086
  }
1058
1087
  unsetLoadingStatus() {
1059
1088
  for (const timeout of this.loadingTimeouts) clearTimeout(timeout);
1060
1089
  this.loadingTimeouts = [];
1061
1090
  const elapsed = this.loadingStartedAt ? Date.now() - this.loadingStartedAt : 0;
1062
- const delay = this.loadingStartedAt ? Math.max(0, MIN_LOADING_MS - elapsed) : 0;
1091
+ const delay = this.loadingStartedAt ? Math.max(0, _DlSelect.minLoadingMs - elapsed) : 0;
1063
1092
  const finish = () => {
1064
1093
  this.appendChild(this.optionsWrapper);
1065
1094
  this.classList.remove("dl-select-loading");
@@ -2106,6 +2135,7 @@ var DeclarativeForm = class _DeclarativeForm {
2106
2135
  closeWith: (action) => this.close(action)
2107
2136
  });
2108
2137
  this.buildFields();
2138
+ this.applyInitialLayout();
2109
2139
  this.ready = this.initialize();
2110
2140
  }
2111
2141
  // ------------------------------------------------------------------ setup
@@ -2127,12 +2157,41 @@ var DeclarativeForm = class _DeclarativeForm {
2127
2157
  );
2128
2158
  }
2129
2159
  const field = this.registry.create(descriptor, context);
2160
+ field.applyStaticDefaultValue();
2161
+ this.applySeedValue(field);
2130
2162
  this.fieldList.push(field);
2131
2163
  this.fieldsByName.set(descriptor.name, field);
2132
2164
  this.tooltips.register(descriptor.name, field.tooltipMarker);
2133
2165
  this.formElement.appendChild(field.wrapper);
2134
2166
  }
2135
2167
  }
2168
+ /**
2169
+ * Decide field visibility and build the tab strip synchronously, before the
2170
+ * caller gets the form back.
2171
+ *
2172
+ * {@link initialize} is async โ€” it awaits option loads and default values โ€”
2173
+ * but `openInModal()` normally runs in the same tick as the constructor.
2174
+ * Without this pass the dialog paints every field of every tab, then collapses
2175
+ * to the active tab once that first async update lands, which is a visible
2176
+ * layout shift as soon as any field loads its options over the network.
2177
+ *
2178
+ * Only visibility is decided here: no `onFormChange`, no `onFormUpdate`, and
2179
+ * the value model is left untouched, so the regular update cycle still sees
2180
+ * the first real change and refreshes the buttons.
2181
+ */
2182
+ applyInitialLayout() {
2183
+ const values = this.getValues();
2184
+ const activeTabs = /* @__PURE__ */ new Set();
2185
+ for (const field of this.fieldList) {
2186
+ const active = field.descriptor.isActive?.({
2187
+ ...this.contextFor(field.descriptor),
2188
+ data: values
2189
+ }) ?? true;
2190
+ field.setActive(active);
2191
+ if (active) for (const tab of field.tabs()) activeTabs.add(tab);
2192
+ }
2193
+ this.renderTabs(activeTabs);
2194
+ }
2136
2195
  async initialize() {
2137
2196
  await Promise.allSettled(
2138
2197
  this.fieldList.map(
@@ -2142,8 +2201,24 @@ var DeclarativeForm = class _DeclarativeForm {
2142
2201
  await Promise.allSettled(
2143
2202
  this.fieldList.map((field) => this.scheduler.track(field.applyDefaultValue()))
2144
2203
  );
2204
+ for (const field of this.fieldList) this.applySeedValue(field);
2145
2205
  await this.update();
2146
2206
  }
2207
+ /**
2208
+ * Seed one field from {@link DeclarativeFormOptions.defaultValues}.
2209
+ *
2210
+ * Run twice: once at build time so the first paint and the `isActive`
2211
+ * predicates behind it already see the record being edited, and once after
2212
+ * the `defaultValue` pass โ€” which is async, and would otherwise overwrite the
2213
+ * seeded value with the field's own default for the create case.
2214
+ */
2215
+ applySeedValue(field) {
2216
+ const values = this.options.defaultValues;
2217
+ if (!values) return;
2218
+ if (!Object.prototype.hasOwnProperty.call(values, field.name)) return;
2219
+ const value = values[field.name];
2220
+ if (value !== void 0) field.setValue(value);
2221
+ }
2147
2222
  /** Resolves once initial options and default values have been applied. */
2148
2223
  whenReady() {
2149
2224
  return this.ready.then(() => this.scheduler.whenSettled());
@@ -2470,6 +2545,44 @@ var DeclarativeForm = class _DeclarativeForm {
2470
2545
  }
2471
2546
  };
2472
2547
 
2548
+ // src/core/ask.ts
2549
+ function ask(fields, options = {}) {
2550
+ return askWith(DeclarativeForm, fields, options);
2551
+ }
2552
+ function askWith(FormClass, fields, options = {}) {
2553
+ const { modal, dismissable = true, buttons, ...formOptions } = options;
2554
+ return new Promise((resolve) => {
2555
+ let settled = false;
2556
+ const settle = (values) => {
2557
+ if (settled) return;
2558
+ settled = true;
2559
+ setTimeout(() => resolve(values), 0);
2560
+ };
2561
+ new FormClass({
2562
+ ...formOptions,
2563
+ fields,
2564
+ buttons: buttons && settleOnClose(buttons, settle),
2565
+ onConfirm: (values) => settle(values),
2566
+ onCancel: dismissable ? () => settle(void 0) : void 0
2567
+ }).openInModal(modal);
2568
+ });
2569
+ }
2570
+ function settleOnClose(buttons, settle) {
2571
+ return Object.fromEntries(
2572
+ Object.entries(buttons).map(([label, descriptor]) => [
2573
+ label,
2574
+ descriptor.doNotCloseModal ? descriptor : {
2575
+ ...descriptor,
2576
+ action: async (values) => {
2577
+ const result = await descriptor.action?.(values);
2578
+ settle(values);
2579
+ return result;
2580
+ }
2581
+ }
2582
+ ])
2583
+ );
2584
+ }
2585
+
2473
2586
  exports.ArrayField = ArrayField;
2474
2587
  exports.ButtonBar = ButtonBar;
2475
2588
  exports.CardsField = CardsField;
@@ -2496,6 +2609,7 @@ exports.TextAreaField = TextAreaField;
2496
2609
  exports.TextField = TextField;
2497
2610
  exports.TooltipController = TooltipController;
2498
2611
  exports.UpdateScheduler = UpdateScheduler;
2612
+ exports.ask = ask;
2499
2613
  exports.deepEqual = deepEqual;
2500
2614
  exports.defaultFieldRegistry = defaultFieldRegistry;
2501
2615
  exports.defineDlSelect = defineDlSelect;