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 +154 -147
- package/dist/index.cjs +119 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -1
- package/dist/index.d.ts +106 -1
- package/dist/index.js +119 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/styles/declarative-forms.css +14 -0
- package/CHANGELOG.md +0 -148
package/README.md
CHANGED
|
@@ -1,102 +1,118 @@
|
|
|
1
1
|
# declarative-forms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Ask the user for an object, the way `prompt()` asks for a string.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
// { title: 'Sunrise 2.0', notes: 'โฆ', reviewers: ['Ada', 'Grace'] }
|
|
26
|
+
```
|
|
10
27
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
## Why a call and not a component
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
69
|
+
```ts
|
|
70
|
+
if (await confirmDetails()) { โฆ }
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
##
|
|
76
|
+
## What that model gives you that a component cannot
|
|
77
77
|
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
```ts
|
|
103
|
+
const updated = await ask(userFields, { defaultValues: user });
|
|
104
|
+
```
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
+
See [the API reference](https://wolfoo2931.github.io/declarative-forms/reference/api).
|
|
179
155
|
|
|
180
|
-
##
|
|
156
|
+
## The ten field kinds
|
|
181
157
|
|
|
182
|
-
|
|
158
|
+
`text` ยท `textarea` ยท `select` (single, multiple, async, searchable) ยท
|
|
159
|
+
`checkbox` ยท `message` ยท `file` ยท `computed` ยท `cards` ยท `custom` ยท `array`
|
|
183
160
|
|
|
184
|
-
|
|
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
|
-
##
|
|
163
|
+
## Where it fits
|
|
191
164
|
|
|
192
|
-
|
|
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
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
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
|
-
##
|
|
198
|
+
## Documentation
|
|
199
199
|
|
|
200
|
-
|
|
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
|
-
|
|
209
|
+
## License
|
|
203
210
|
|
|
204
|
-
|
|
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
|
-
},
|
|
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,
|
|
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;
|