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/dist/index.d.cts CHANGED
@@ -262,6 +262,21 @@ interface ButtonDescriptor {
262
262
  type ButtonMap = Record<string, ButtonDescriptor>;
263
263
  interface DeclarativeFormOptions {
264
264
  fields: readonly FieldDescriptor[];
265
+ /**
266
+ * The record to open the form with, keyed by field name — how you edit an
267
+ * existing object rather than asking for a new one.
268
+ *
269
+ * ```ts
270
+ * const updated = await ask(userFields, { defaultValues: user });
271
+ * ```
272
+ *
273
+ * Each entry wins over that field's own `defaultValue`, so the same field
274
+ * list serves both the "create" and the "edit" call. Keys naming no field are
275
+ * ignored — pass a whole domain object without filtering it first — as are
276
+ * keys whose value is `undefined`, which fall back to the field's
277
+ * `defaultValue`. An `array` field takes its list of records here directly.
278
+ */
279
+ defaultValues?: FormValues;
265
280
  /**
266
281
  * Buttons for the dialog's lower bar, keyed by label.
267
282
  * Defaults to a single `OK` button running {@link onConfirm}.
@@ -507,6 +522,16 @@ declare abstract class Field<D extends BaseFieldDescriptor = FieldDescriptor> im
507
522
  * resolved before being applied.
508
523
  */
509
524
  applyDefaultValue(): Promise<void>;
525
+ /**
526
+ * Apply a plainly declared default value straight away.
527
+ *
528
+ * {@link applyDefaultValue} is awaited during initialisation, so a value it
529
+ * applies cannot be seen by the first layout pass. A default that is neither a
530
+ * function nor a promise needs no awaiting, so it is applied at build time and
531
+ * `isActive` predicates reading it are right from the first paint. The async
532
+ * pass runs afterwards regardless and simply sets the same value again.
533
+ */
534
+ applyStaticDefaultValue(): void;
510
535
  /**
511
536
  * Hook for setup that cannot happen in {@link build} — loading select
512
537
  * options, or a first render that needs the form context. May be synchronous.
@@ -671,7 +696,31 @@ declare class DeclarativeForm implements SubForm, StackableDialog {
671
696
  constructor(options: DeclarativeFormOptions, internal?: InternalOptions);
672
697
  private resolveButtons;
673
698
  private buildFields;
699
+ /**
700
+ * Decide field visibility and build the tab strip synchronously, before the
701
+ * caller gets the form back.
702
+ *
703
+ * {@link initialize} is async — it awaits option loads and default values —
704
+ * but `openInModal()` normally runs in the same tick as the constructor.
705
+ * Without this pass the dialog paints every field of every tab, then collapses
706
+ * to the active tab once that first async update lands, which is a visible
707
+ * layout shift as soon as any field loads its options over the network.
708
+ *
709
+ * Only visibility is decided here: no `onFormChange`, no `onFormUpdate`, and
710
+ * the value model is left untouched, so the regular update cycle still sees
711
+ * the first real change and refreshes the buttons.
712
+ */
713
+ private applyInitialLayout;
674
714
  private initialize;
715
+ /**
716
+ * Seed one field from {@link DeclarativeFormOptions.defaultValues}.
717
+ *
718
+ * Run twice: once at build time so the first paint and the `isActive`
719
+ * predicates behind it already see the record being edited, and once after
720
+ * the `defaultValue` pass — which is async, and would otherwise overwrite the
721
+ * seeded value with the field's own default for the create case.
722
+ */
723
+ private applySeedValue;
675
724
  /** Resolves once initial options and default values have been applied. */
676
725
  whenReady(): Promise<void>;
677
726
  private createFieldContext;
@@ -761,6 +810,46 @@ declare class DeclarativeForm implements SubForm, StackableDialog {
761
810
  resetTooltips(fieldNames: Iterable<string>): void;
762
811
  }
763
812
 
813
+ /**
814
+ * Everything {@link DeclarativeForm} accepts, minus the three things `ask`
815
+ * owns: the fields it is given, and the two callbacks it turns into a promise.
816
+ */
817
+ interface AskOptions extends Omit<DeclarativeFormOptions, 'fields' | 'onConfirm' | 'onCancel'> {
818
+ /** Class names for the dialog chrome, as passed to `openInModal`. */
819
+ modal?: OpenInModalOptions;
820
+ /**
821
+ * Whether the dialog can be dismissed with Esc or the close button.
822
+ *
823
+ * Defaults to `true`. When `false` no close button is rendered and the
824
+ * promise can only settle with values — so only turn it off when a button of
825
+ * yours is guaranteed to close the dialog.
826
+ */
827
+ dismissable?: boolean;
828
+ }
829
+ /**
830
+ * Ask the user for a record, the way `prompt()` asks for a string.
831
+ *
832
+ * Opens the fields as a modal dialog and resolves with its values, or with
833
+ * `undefined` when the user dismisses it.
834
+ *
835
+ * ```ts
836
+ * const release = await ask([
837
+ * { name: 'title', displayName: 'Release title' },
838
+ * { name: 'notes', kind: 'textarea', displayName: 'What changed' },
839
+ * ]);
840
+ *
841
+ * if (release) publish(release);
842
+ * ```
843
+ *
844
+ * With custom {@link AskOptions.buttons | buttons}, the promise resolves when
845
+ * any button that closes the dialog has run its action; a `doNotCloseModal`
846
+ * button (a wizard's "Next") leaves it pending, as it leaves the dialog open.
847
+ *
848
+ * Use `new DeclarativeForm(…)` directly when you need the form object itself —
849
+ * to embed it in a page, subscribe to its values, or close it from elsewhere.
850
+ */
851
+ declare function ask(fields: readonly FieldDescriptor[], options?: AskOptions): Promise<FormValues | undefined>;
852
+
764
853
  /**
765
854
  * A repeating list of sub-records, each edited in its own nested dialog.
766
855
  *
@@ -971,6 +1060,22 @@ declare class DlOption extends CustomElementBase {
971
1060
  * `document.body` while open so it can escape a scrolling container.
972
1061
  */
973
1062
  declare class DlSelect extends CustomElementBase {
1063
+ /**
1064
+ * How long a load may run before the skeleton is shown at all. A load that
1065
+ * finishes inside this window never flashes one.
1066
+ */
1067
+ static showLoadingAfterMs: number;
1068
+ /**
1069
+ * Once the skeleton *has* been shown, it is held for at least this long, so
1070
+ * it cannot appear and vanish in the same instant.
1071
+ *
1072
+ * Both are statics so a test suite can set them to 0. They are deliberate
1073
+ * pauses for human eyes; to a test they are pure latency, and they are why
1074
+ * suites end up sleeping around these selects instead of waiting on a
1075
+ * condition. Wait on `isLoading` rather than the `dl-select-loading` class:
1076
+ * `isLoading` flips synchronously, the class lags by `showLoadingAfterMs`.
1077
+ */
1078
+ static minLoadingMs: number;
974
1079
  readonly optionsWrapper: HTMLDivElement;
975
1080
  readonly inputWrapper: HTMLSpanElement;
976
1081
  readonly noMatchesHint: HTMLSpanElement;
@@ -1232,4 +1337,4 @@ declare class NativeTooltipProvider implements TooltipProvider {
1232
1337
  */
1233
1338
  declare function deepEqual(a: unknown, b: unknown): boolean;
1234
1339
 
1235
- export { ArrayField, type ArrayFieldDescriptor, type BaseFieldDescriptor, ButtonBar, type ButtonContext, type ButtonDescriptor, type ButtonMap, CardsField, type CardsFieldDescriptor, CheckboxField, type CheckboxFieldDescriptor, ComputedField, type ComputedFieldDescriptor, CustomField, type CustomFieldDescriptor, DL_SELECT_STYLES, DeclarativeForm, type DeclarativeFormOptions, DlOption, DlSelect, Field, type FieldConstructor, type FieldContext, type FieldDescriptor, type FieldHandle, type FieldKind, type FieldMessage, FieldRegistry, FileField, type FileFieldDescriptor, type FormChangeContext, type FormContext, FormModel, type FormValues, IdGenerator, MessageField, type MessageFieldDescriptor, ModalStack, ModalView, NativeTooltipProvider, type OpenInModalOptions, type OptionsErrorContext, type Reactive, type RenderContext, SafeHtml, SelectField, type SelectFieldDescriptor, type SelectOption, type StackableDialog, type SubForm, type SubFormOptions, TabBar, type TabSpec, TextAreaField, type TextAreaFieldDescriptor, TextField, type TextFieldDescriptor, type TextOrHtml, TooltipController, type TooltipDescriptor, type TooltipProvider, UpdateScheduler, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
1340
+ export { ArrayField, type ArrayFieldDescriptor, type AskOptions, type BaseFieldDescriptor, ButtonBar, type ButtonContext, type ButtonDescriptor, type ButtonMap, CardsField, type CardsFieldDescriptor, CheckboxField, type CheckboxFieldDescriptor, ComputedField, type ComputedFieldDescriptor, CustomField, type CustomFieldDescriptor, DL_SELECT_STYLES, DeclarativeForm, type DeclarativeFormOptions, DlOption, DlSelect, Field, type FieldConstructor, type FieldContext, type FieldDescriptor, type FieldHandle, type FieldKind, type FieldMessage, FieldRegistry, FileField, type FileFieldDescriptor, type FormChangeContext, type FormContext, FormModel, type FormValues, IdGenerator, MessageField, type MessageFieldDescriptor, ModalStack, ModalView, NativeTooltipProvider, type OpenInModalOptions, type OptionsErrorContext, type Reactive, type RenderContext, SafeHtml, SelectField, type SelectFieldDescriptor, type SelectOption, type StackableDialog, type SubForm, type SubFormOptions, TabBar, type TabSpec, TextAreaField, type TextAreaFieldDescriptor, TextField, type TextFieldDescriptor, type TextOrHtml, TooltipController, type TooltipDescriptor, type TooltipProvider, UpdateScheduler, ask, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
package/dist/index.d.ts CHANGED
@@ -262,6 +262,21 @@ interface ButtonDescriptor {
262
262
  type ButtonMap = Record<string, ButtonDescriptor>;
263
263
  interface DeclarativeFormOptions {
264
264
  fields: readonly FieldDescriptor[];
265
+ /**
266
+ * The record to open the form with, keyed by field name — how you edit an
267
+ * existing object rather than asking for a new one.
268
+ *
269
+ * ```ts
270
+ * const updated = await ask(userFields, { defaultValues: user });
271
+ * ```
272
+ *
273
+ * Each entry wins over that field's own `defaultValue`, so the same field
274
+ * list serves both the "create" and the "edit" call. Keys naming no field are
275
+ * ignored — pass a whole domain object without filtering it first — as are
276
+ * keys whose value is `undefined`, which fall back to the field's
277
+ * `defaultValue`. An `array` field takes its list of records here directly.
278
+ */
279
+ defaultValues?: FormValues;
265
280
  /**
266
281
  * Buttons for the dialog's lower bar, keyed by label.
267
282
  * Defaults to a single `OK` button running {@link onConfirm}.
@@ -507,6 +522,16 @@ declare abstract class Field<D extends BaseFieldDescriptor = FieldDescriptor> im
507
522
  * resolved before being applied.
508
523
  */
509
524
  applyDefaultValue(): Promise<void>;
525
+ /**
526
+ * Apply a plainly declared default value straight away.
527
+ *
528
+ * {@link applyDefaultValue} is awaited during initialisation, so a value it
529
+ * applies cannot be seen by the first layout pass. A default that is neither a
530
+ * function nor a promise needs no awaiting, so it is applied at build time and
531
+ * `isActive` predicates reading it are right from the first paint. The async
532
+ * pass runs afterwards regardless and simply sets the same value again.
533
+ */
534
+ applyStaticDefaultValue(): void;
510
535
  /**
511
536
  * Hook for setup that cannot happen in {@link build} — loading select
512
537
  * options, or a first render that needs the form context. May be synchronous.
@@ -671,7 +696,31 @@ declare class DeclarativeForm implements SubForm, StackableDialog {
671
696
  constructor(options: DeclarativeFormOptions, internal?: InternalOptions);
672
697
  private resolveButtons;
673
698
  private buildFields;
699
+ /**
700
+ * Decide field visibility and build the tab strip synchronously, before the
701
+ * caller gets the form back.
702
+ *
703
+ * {@link initialize} is async — it awaits option loads and default values —
704
+ * but `openInModal()` normally runs in the same tick as the constructor.
705
+ * Without this pass the dialog paints every field of every tab, then collapses
706
+ * to the active tab once that first async update lands, which is a visible
707
+ * layout shift as soon as any field loads its options over the network.
708
+ *
709
+ * Only visibility is decided here: no `onFormChange`, no `onFormUpdate`, and
710
+ * the value model is left untouched, so the regular update cycle still sees
711
+ * the first real change and refreshes the buttons.
712
+ */
713
+ private applyInitialLayout;
674
714
  private initialize;
715
+ /**
716
+ * Seed one field from {@link DeclarativeFormOptions.defaultValues}.
717
+ *
718
+ * Run twice: once at build time so the first paint and the `isActive`
719
+ * predicates behind it already see the record being edited, and once after
720
+ * the `defaultValue` pass — which is async, and would otherwise overwrite the
721
+ * seeded value with the field's own default for the create case.
722
+ */
723
+ private applySeedValue;
675
724
  /** Resolves once initial options and default values have been applied. */
676
725
  whenReady(): Promise<void>;
677
726
  private createFieldContext;
@@ -761,6 +810,46 @@ declare class DeclarativeForm implements SubForm, StackableDialog {
761
810
  resetTooltips(fieldNames: Iterable<string>): void;
762
811
  }
763
812
 
813
+ /**
814
+ * Everything {@link DeclarativeForm} accepts, minus the three things `ask`
815
+ * owns: the fields it is given, and the two callbacks it turns into a promise.
816
+ */
817
+ interface AskOptions extends Omit<DeclarativeFormOptions, 'fields' | 'onConfirm' | 'onCancel'> {
818
+ /** Class names for the dialog chrome, as passed to `openInModal`. */
819
+ modal?: OpenInModalOptions;
820
+ /**
821
+ * Whether the dialog can be dismissed with Esc or the close button.
822
+ *
823
+ * Defaults to `true`. When `false` no close button is rendered and the
824
+ * promise can only settle with values — so only turn it off when a button of
825
+ * yours is guaranteed to close the dialog.
826
+ */
827
+ dismissable?: boolean;
828
+ }
829
+ /**
830
+ * Ask the user for a record, the way `prompt()` asks for a string.
831
+ *
832
+ * Opens the fields as a modal dialog and resolves with its values, or with
833
+ * `undefined` when the user dismisses it.
834
+ *
835
+ * ```ts
836
+ * const release = await ask([
837
+ * { name: 'title', displayName: 'Release title' },
838
+ * { name: 'notes', kind: 'textarea', displayName: 'What changed' },
839
+ * ]);
840
+ *
841
+ * if (release) publish(release);
842
+ * ```
843
+ *
844
+ * With custom {@link AskOptions.buttons | buttons}, the promise resolves when
845
+ * any button that closes the dialog has run its action; a `doNotCloseModal`
846
+ * button (a wizard's "Next") leaves it pending, as it leaves the dialog open.
847
+ *
848
+ * Use `new DeclarativeForm(…)` directly when you need the form object itself —
849
+ * to embed it in a page, subscribe to its values, or close it from elsewhere.
850
+ */
851
+ declare function ask(fields: readonly FieldDescriptor[], options?: AskOptions): Promise<FormValues | undefined>;
852
+
764
853
  /**
765
854
  * A repeating list of sub-records, each edited in its own nested dialog.
766
855
  *
@@ -971,6 +1060,22 @@ declare class DlOption extends CustomElementBase {
971
1060
  * `document.body` while open so it can escape a scrolling container.
972
1061
  */
973
1062
  declare class DlSelect extends CustomElementBase {
1063
+ /**
1064
+ * How long a load may run before the skeleton is shown at all. A load that
1065
+ * finishes inside this window never flashes one.
1066
+ */
1067
+ static showLoadingAfterMs: number;
1068
+ /**
1069
+ * Once the skeleton *has* been shown, it is held for at least this long, so
1070
+ * it cannot appear and vanish in the same instant.
1071
+ *
1072
+ * Both are statics so a test suite can set them to 0. They are deliberate
1073
+ * pauses for human eyes; to a test they are pure latency, and they are why
1074
+ * suites end up sleeping around these selects instead of waiting on a
1075
+ * condition. Wait on `isLoading` rather than the `dl-select-loading` class:
1076
+ * `isLoading` flips synchronously, the class lags by `showLoadingAfterMs`.
1077
+ */
1078
+ static minLoadingMs: number;
974
1079
  readonly optionsWrapper: HTMLDivElement;
975
1080
  readonly inputWrapper: HTMLSpanElement;
976
1081
  readonly noMatchesHint: HTMLSpanElement;
@@ -1232,4 +1337,4 @@ declare class NativeTooltipProvider implements TooltipProvider {
1232
1337
  */
1233
1338
  declare function deepEqual(a: unknown, b: unknown): boolean;
1234
1339
 
1235
- export { ArrayField, type ArrayFieldDescriptor, type BaseFieldDescriptor, ButtonBar, type ButtonContext, type ButtonDescriptor, type ButtonMap, CardsField, type CardsFieldDescriptor, CheckboxField, type CheckboxFieldDescriptor, ComputedField, type ComputedFieldDescriptor, CustomField, type CustomFieldDescriptor, DL_SELECT_STYLES, DeclarativeForm, type DeclarativeFormOptions, DlOption, DlSelect, Field, type FieldConstructor, type FieldContext, type FieldDescriptor, type FieldHandle, type FieldKind, type FieldMessage, FieldRegistry, FileField, type FileFieldDescriptor, type FormChangeContext, type FormContext, FormModel, type FormValues, IdGenerator, MessageField, type MessageFieldDescriptor, ModalStack, ModalView, NativeTooltipProvider, type OpenInModalOptions, type OptionsErrorContext, type Reactive, type RenderContext, SafeHtml, SelectField, type SelectFieldDescriptor, type SelectOption, type StackableDialog, type SubForm, type SubFormOptions, TabBar, type TabSpec, TextAreaField, type TextAreaFieldDescriptor, TextField, type TextFieldDescriptor, type TextOrHtml, TooltipController, type TooltipDescriptor, type TooltipProvider, UpdateScheduler, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
1340
+ export { ArrayField, type ArrayFieldDescriptor, type AskOptions, type BaseFieldDescriptor, ButtonBar, type ButtonContext, type ButtonDescriptor, type ButtonMap, CardsField, type CardsFieldDescriptor, CheckboxField, type CheckboxFieldDescriptor, ComputedField, type ComputedFieldDescriptor, CustomField, type CustomFieldDescriptor, DL_SELECT_STYLES, DeclarativeForm, type DeclarativeFormOptions, DlOption, DlSelect, Field, type FieldConstructor, type FieldContext, type FieldDescriptor, type FieldHandle, type FieldKind, type FieldMessage, FieldRegistry, FileField, type FileFieldDescriptor, type FormChangeContext, type FormContext, FormModel, type FormValues, IdGenerator, MessageField, type MessageFieldDescriptor, ModalStack, ModalView, NativeTooltipProvider, type OpenInModalOptions, type OptionsErrorContext, type Reactive, type RenderContext, SafeHtml, SelectField, type SelectFieldDescriptor, type SelectOption, type StackableDialog, type SubForm, type SubFormOptions, TabBar, type TabSpec, TextAreaField, type TextAreaFieldDescriptor, TextField, type TextFieldDescriptor, type TextOrHtml, TooltipController, type TooltipDescriptor, type TooltipProvider, UpdateScheduler, ask, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
package/dist/index.js CHANGED
@@ -193,6 +193,21 @@ var Field = class {
193
193
  );
194
194
  if (value !== void 0) this.setValue(value);
195
195
  }
196
+ /**
197
+ * Apply a plainly declared default value straight away.
198
+ *
199
+ * {@link applyDefaultValue} is awaited during initialisation, so a value it
200
+ * applies cannot be seen by the first layout pass. A default that is neither a
201
+ * function nor a promise needs no awaiting, so it is applied at build time and
202
+ * `isActive` predicates reading it are right from the first paint. The async
203
+ * pass runs afterwards regardless and simply sets the same value again.
204
+ */
205
+ applyStaticDefaultValue() {
206
+ const value = this.descriptor.defaultValue;
207
+ if (value === void 0 || typeof value === "function") return;
208
+ if (value instanceof Promise) return;
209
+ this.setValue(value);
210
+ }
196
211
  /**
197
212
  * Hook for setup that cannot happen in {@link build} — loading select
198
213
  * options, or a first render that needs the form context. May be synchronous.
@@ -927,8 +942,6 @@ function injectStyles() {
927
942
  // src/components/DlSelect.ts
928
943
  var SVG_NS = "http://www.w3.org/2000/svg";
929
944
  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";
930
- var MIN_LOADING_MS = 1500;
931
- var LOADING_DELAY_MS = 100;
932
945
  var CustomElementBase = typeof HTMLElement === "undefined" ? class {
933
946
  } : HTMLElement;
934
947
  var DlOption = class extends CustomElementBase {
@@ -941,7 +954,23 @@ var DlOption = class extends CustomElementBase {
941
954
  return this.getAttribute("displayWhenSelected") ?? this.innerText;
942
955
  }
943
956
  };
944
- var DlSelect = class extends CustomElementBase {
957
+ var DlSelect = class _DlSelect extends CustomElementBase {
958
+ /**
959
+ * How long a load may run before the skeleton is shown at all. A load that
960
+ * finishes inside this window never flashes one.
961
+ */
962
+ static showLoadingAfterMs = 100;
963
+ /**
964
+ * Once the skeleton *has* been shown, it is held for at least this long, so
965
+ * it cannot appear and vanish in the same instant.
966
+ *
967
+ * Both are statics so a test suite can set them to 0. They are deliberate
968
+ * pauses for human eyes; to a test they are pure latency, and they are why
969
+ * suites end up sleeping around these selects instead of waiting on a
970
+ * condition. Wait on `isLoading` rather than the `dl-select-loading` class:
971
+ * `isLoading` flips synchronously, the class lags by `showLoadingAfterMs`.
972
+ */
973
+ static minLoadingMs = 1500;
945
974
  optionsWrapper;
946
975
  inputWrapper;
947
976
  noMatchesHint;
@@ -1050,14 +1079,14 @@ var DlSelect = class extends CustomElementBase {
1050
1079
  this.classList.add("dl-select-loading");
1051
1080
  this.optionsWrapper.remove();
1052
1081
  this.loadingStartedAt = Date.now();
1053
- }, LOADING_DELAY_MS)
1082
+ }, _DlSelect.showLoadingAfterMs)
1054
1083
  );
1055
1084
  }
1056
1085
  unsetLoadingStatus() {
1057
1086
  for (const timeout of this.loadingTimeouts) clearTimeout(timeout);
1058
1087
  this.loadingTimeouts = [];
1059
1088
  const elapsed = this.loadingStartedAt ? Date.now() - this.loadingStartedAt : 0;
1060
- const delay = this.loadingStartedAt ? Math.max(0, MIN_LOADING_MS - elapsed) : 0;
1089
+ const delay = this.loadingStartedAt ? Math.max(0, _DlSelect.minLoadingMs - elapsed) : 0;
1061
1090
  const finish = () => {
1062
1091
  this.appendChild(this.optionsWrapper);
1063
1092
  this.classList.remove("dl-select-loading");
@@ -2104,6 +2133,7 @@ var DeclarativeForm = class _DeclarativeForm {
2104
2133
  closeWith: (action) => this.close(action)
2105
2134
  });
2106
2135
  this.buildFields();
2136
+ this.applyInitialLayout();
2107
2137
  this.ready = this.initialize();
2108
2138
  }
2109
2139
  // ------------------------------------------------------------------ setup
@@ -2125,12 +2155,41 @@ var DeclarativeForm = class _DeclarativeForm {
2125
2155
  );
2126
2156
  }
2127
2157
  const field = this.registry.create(descriptor, context);
2158
+ field.applyStaticDefaultValue();
2159
+ this.applySeedValue(field);
2128
2160
  this.fieldList.push(field);
2129
2161
  this.fieldsByName.set(descriptor.name, field);
2130
2162
  this.tooltips.register(descriptor.name, field.tooltipMarker);
2131
2163
  this.formElement.appendChild(field.wrapper);
2132
2164
  }
2133
2165
  }
2166
+ /**
2167
+ * Decide field visibility and build the tab strip synchronously, before the
2168
+ * caller gets the form back.
2169
+ *
2170
+ * {@link initialize} is async — it awaits option loads and default values —
2171
+ * but `openInModal()` normally runs in the same tick as the constructor.
2172
+ * Without this pass the dialog paints every field of every tab, then collapses
2173
+ * to the active tab once that first async update lands, which is a visible
2174
+ * layout shift as soon as any field loads its options over the network.
2175
+ *
2176
+ * Only visibility is decided here: no `onFormChange`, no `onFormUpdate`, and
2177
+ * the value model is left untouched, so the regular update cycle still sees
2178
+ * the first real change and refreshes the buttons.
2179
+ */
2180
+ applyInitialLayout() {
2181
+ const values = this.getValues();
2182
+ const activeTabs = /* @__PURE__ */ new Set();
2183
+ for (const field of this.fieldList) {
2184
+ const active = field.descriptor.isActive?.({
2185
+ ...this.contextFor(field.descriptor),
2186
+ data: values
2187
+ }) ?? true;
2188
+ field.setActive(active);
2189
+ if (active) for (const tab of field.tabs()) activeTabs.add(tab);
2190
+ }
2191
+ this.renderTabs(activeTabs);
2192
+ }
2134
2193
  async initialize() {
2135
2194
  await Promise.allSettled(
2136
2195
  this.fieldList.map(
@@ -2140,8 +2199,24 @@ var DeclarativeForm = class _DeclarativeForm {
2140
2199
  await Promise.allSettled(
2141
2200
  this.fieldList.map((field) => this.scheduler.track(field.applyDefaultValue()))
2142
2201
  );
2202
+ for (const field of this.fieldList) this.applySeedValue(field);
2143
2203
  await this.update();
2144
2204
  }
2205
+ /**
2206
+ * Seed one field from {@link DeclarativeFormOptions.defaultValues}.
2207
+ *
2208
+ * Run twice: once at build time so the first paint and the `isActive`
2209
+ * predicates behind it already see the record being edited, and once after
2210
+ * the `defaultValue` pass — which is async, and would otherwise overwrite the
2211
+ * seeded value with the field's own default for the create case.
2212
+ */
2213
+ applySeedValue(field) {
2214
+ const values = this.options.defaultValues;
2215
+ if (!values) return;
2216
+ if (!Object.prototype.hasOwnProperty.call(values, field.name)) return;
2217
+ const value = values[field.name];
2218
+ if (value !== void 0) field.setValue(value);
2219
+ }
2145
2220
  /** Resolves once initial options and default values have been applied. */
2146
2221
  whenReady() {
2147
2222
  return this.ready.then(() => this.scheduler.whenSettled());
@@ -2468,6 +2543,44 @@ var DeclarativeForm = class _DeclarativeForm {
2468
2543
  }
2469
2544
  };
2470
2545
 
2471
- export { ArrayField, ButtonBar, CardsField, CheckboxField, ComputedField, CustomField, DL_SELECT_STYLES, DeclarativeForm, DlOption, DlSelect, Field, FieldRegistry, FileField, FormModel, IdGenerator, MessageField, ModalStack, ModalView, NativeTooltipProvider, SafeHtml, SelectField, TabBar, TextAreaField, TextField, TooltipController, UpdateScheduler, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
2546
+ // src/core/ask.ts
2547
+ function ask(fields, options = {}) {
2548
+ return askWith(DeclarativeForm, fields, options);
2549
+ }
2550
+ function askWith(FormClass, fields, options = {}) {
2551
+ const { modal, dismissable = true, buttons, ...formOptions } = options;
2552
+ return new Promise((resolve) => {
2553
+ let settled = false;
2554
+ const settle = (values) => {
2555
+ if (settled) return;
2556
+ settled = true;
2557
+ setTimeout(() => resolve(values), 0);
2558
+ };
2559
+ new FormClass({
2560
+ ...formOptions,
2561
+ fields,
2562
+ buttons: buttons && settleOnClose(buttons, settle),
2563
+ onConfirm: (values) => settle(values),
2564
+ onCancel: dismissable ? () => settle(void 0) : void 0
2565
+ }).openInModal(modal);
2566
+ });
2567
+ }
2568
+ function settleOnClose(buttons, settle) {
2569
+ return Object.fromEntries(
2570
+ Object.entries(buttons).map(([label, descriptor]) => [
2571
+ label,
2572
+ descriptor.doNotCloseModal ? descriptor : {
2573
+ ...descriptor,
2574
+ action: async (values) => {
2575
+ const result = await descriptor.action?.(values);
2576
+ settle(values);
2577
+ return result;
2578
+ }
2579
+ }
2580
+ ])
2581
+ );
2582
+ }
2583
+
2584
+ export { ArrayField, ButtonBar, CardsField, CheckboxField, ComputedField, CustomField, DL_SELECT_STYLES, DeclarativeForm, DlOption, DlSelect, Field, FieldRegistry, FileField, FormModel, IdGenerator, MessageField, ModalStack, ModalView, NativeTooltipProvider, SafeHtml, SelectField, TabBar, TextAreaField, TextField, TooltipController, UpdateScheduler, ask, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
2472
2585
  //# sourceMappingURL=index.js.map
2473
2586
  //# sourceMappingURL=index.js.map