vanilla-jui 1.4.7 → 1.5.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.
Files changed (52) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +418 -970
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +466 -346
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +198 -328
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +273 -367
  13. package/src/components/pagination.ts +201 -375
  14. package/src/components/swiper.ts +689 -1107
  15. package/src/components/tabs.ts +467 -778
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/content.css +18 -24
  24. package/src/css/drop.css +4 -1
  25. package/src/css/form.css +8 -0
  26. package/src/css/menu.css +10 -5
  27. package/src/css/modal.css +2 -1
  28. package/src/css/offcanvas.css +5 -17
  29. package/src/css/swiper.css +2 -1
  30. package/src/css/tabs.css +8 -17
  31. package/src/css/themes/default/accordion.css +0 -48
  32. package/src/css/themes/default/swiper.css +1 -1
  33. package/src/css/themes/default/tabs.css +4 -6
  34. package/src/css/toast.css +1 -17
  35. package/src/css/tooltip.css +2 -1
  36. package/src/index.ts +8 -2
  37. package/src/locales/index.ts +3 -0
  38. package/src/primitives/drop.ts +253 -262
  39. package/src/primitives/loading.ts +22 -4
  40. package/src/primitives/parabola.ts +123 -164
  41. package/src/primitives/sticky.ts +297 -0
  42. package/src/primitives/theme.ts +164 -222
  43. package/src/primitives/toast.ts +309 -284
  44. package/src/primitives/tooltip.ts +65 -66
  45. package/src/utilities/browser.ts +38 -0
  46. package/src/utilities/dom.ts +0 -54
  47. package/src/utilities/refs.ts +61 -0
  48. package/src/utilities/state.ts +73 -0
  49. package/src/utilities/timer.ts +11 -31
  50. package/src/validation/validator.ts +289 -389
  51. package/src/components/sticky.ts +0 -297
  52. package/src/core/Component.ts +0 -377
@@ -1,18 +1,21 @@
1
1
  import {
2
+ For,
2
3
  createDeepStore,
3
- createRoot,
4
+ createEffect,
5
+ createMemo,
6
+ createSignal,
4
7
  flushSync,
5
8
  jsx,
6
- onCleanup,
7
- render,
8
9
  } from 'vanilla-signal';
9
10
 
10
- import Component, { type ComponentDOM } from '../core/Component.ts';
11
+ import {
12
+ type FunctionalComponent,
13
+ defineComponent,
14
+ } from '../core/component.ts';
11
15
  import { joinClasses } from '../utilities/class-name.ts';
12
16
  import {
13
17
  type RenderableContent,
14
18
  all,
15
- normalizeRenderableContentNodes,
16
19
  } from '../utilities/dom.ts';
17
20
  import { randomId } from '../utilities/id.ts';
18
21
  import { isPlainObject } from '../utilities/object.ts';
@@ -22,22 +25,23 @@ import {
22
25
  validateParam,
23
26
  } from '../utilities/types.ts';
24
27
  import { createValidator } from '../validation/validator.ts';
28
+ import { createLoading } from '../primitives/loading.ts';
25
29
 
26
30
  type FormValue = string | number | boolean;
27
- type FormOptionInput = FormValue | FormOption;
28
- type FormControlElement =
29
- | HTMLInputElement
30
- | HTMLSelectElement
31
- | HTMLTextAreaElement;
31
+ export type FieldOption = FormValue | FormOption;
32
32
  type FormStyle = string | Partial<CSSStyleDeclaration> | null;
33
33
  export type FormDataValue = FormDataEntryValue | FormDataEntryValue[];
34
34
  export type FormDataRecord = Record<string, FormDataValue>;
35
+ export type FormItemType = FormControlType;
35
36
  type FormControlType =
36
37
  | 'checkbox'
37
38
  | 'custom'
39
+ | 'email'
40
+ | 'password'
38
41
  | 'radio'
39
42
  | 'select'
40
43
  | 'switch'
44
+ | 'text'
41
45
  | 'textarea'
42
46
  | (string & {});
43
47
 
@@ -80,12 +84,24 @@ export interface FormOption {
80
84
  disabled?: boolean;
81
85
  }
82
86
 
87
+ export type FormItemNext = (
88
+ current: FormItem,
89
+ acients: FormItem[]
90
+ ) => FormItem | null;
91
+
92
+ export interface FormItem<TPayload = FormField> {
93
+ id?: string;
94
+ type: FormItemType;
95
+ payload: TPayload;
96
+ next?: FormItemNext | null;
97
+ }
98
+
83
99
  export interface FormField {
100
+ [key: string]: unknown;
84
101
  id?: string;
85
102
  label?: RenderableContent<Form> | false;
86
103
  name?: string;
87
- type?: FormControlType;
88
- options?: readonly FormOptionInput[];
104
+ options?: readonly FieldOption[];
89
105
  value?: FormDataEntryValue | boolean | readonly FormDataEntryValue[];
90
106
  checked?: boolean;
91
107
  required?: boolean;
@@ -125,7 +141,7 @@ export interface FormProps extends Record<string, unknown> {
125
141
  vertical?: boolean;
126
142
  itemVertical?: boolean;
127
143
  style?: FormStyle;
128
- fields?: readonly FormField[];
144
+ fields?: readonly FormItem<FormField>[];
129
145
  buttons?: boolean | readonly FormButton[];
130
146
  className?: FormClassNameConfig;
131
147
  validator?: FormValidatorConfig;
@@ -140,7 +156,7 @@ interface ResolvedFormProps extends Record<string, unknown> {
140
156
  vertical: boolean;
141
157
  itemVertical: boolean;
142
158
  style: FormStyle;
143
- fields: FormField[];
159
+ fields: FormItem<FormField>[];
144
160
  buttons: FormButton[];
145
161
  className: FormClassNames;
146
162
  validator: FormValidatorConfig;
@@ -153,18 +169,13 @@ interface FormState extends ResolvedFormProps {
153
169
  data: FormDataRecord | null;
154
170
  }
155
171
 
156
- interface FormDOM extends ComponentDOM {
157
- root: HTMLFormElement | null;
158
- fields: Map<string, FormControlElement>;
159
- }
160
-
161
172
  interface FormCache {
162
173
  initial: ResolvedFormProps;
163
174
  fieldIds: Map<string | number, string>;
164
175
  }
165
176
 
166
177
  interface ValidatorInstance {
167
- dom: { root: Element | null };
178
+ readonly element: Element | null;
168
179
  props: (FormValidatorConfig & { onSubmit: null }) | null;
169
180
  validate: () => boolean;
170
181
  reset: () => void;
@@ -175,6 +186,7 @@ interface FormControlContext {
175
186
  form: Form;
176
187
  field: FormField;
177
188
  index: number;
189
+ item: FormItem<FormField>;
178
190
  }
179
191
 
180
192
  const DEFAULT_CLASS_NAMES: FormClassNames = {
@@ -211,6 +223,9 @@ const DEFAULT_BUTTONS: FormButton[] = [
211
223
  { type: 'reset', text: 'Reset', theme: 'ghost', action: 'reset' },
212
224
  ];
213
225
 
226
+ // Guard against malformed dynamic chains that never terminate.
227
+ const FORM_ITEM_LIMIT = 1000;
228
+
214
229
  const FORM_PROPS_SCHEMA = {
215
230
  id: {
216
231
  default: null,
@@ -247,20 +262,35 @@ const FORM_PROPS_SCHEMA = {
247
262
  } satisfies ResolveSchema<FormProps>;
248
263
 
249
264
  function cloneOptions(
250
- options: readonly FormOptionInput[] | undefined
251
- ): FormOptionInput[] {
265
+ options: readonly FieldOption[] | undefined
266
+ ): FieldOption[] {
252
267
  if (!Array.isArray(options)) return [];
253
268
  return options.map((option) =>
254
269
  option && typeof option === 'object' ? { ...option } : option
255
270
  );
256
271
  }
257
272
 
258
- function cloneFields(fields: readonly FormField[] | undefined): FormField[] {
259
- if (!Array.isArray(fields)) return [];
260
- return fields.map((field) => ({
273
+ function cloneField(field: FormField): FormField {
274
+ return {
261
275
  ...field,
262
276
  options: cloneOptions(field.options),
263
- }));
277
+ };
278
+ }
279
+
280
+ function cloneFormItem(item: FormItem<FormField>): FormItem<FormField> {
281
+ return {
282
+ ...(item.id ? { id: item.id } : {}),
283
+ type: item.type,
284
+ payload: cloneField(item.payload),
285
+ next: item.next || null,
286
+ };
287
+ }
288
+
289
+ function cloneFields(
290
+ fields: readonly FormItem<FormField>[] | undefined
291
+ ): FormItem<FormField>[] {
292
+ if (!Array.isArray(fields)) return [];
293
+ return fields.map((item) => cloneFormItem(item));
264
294
  }
265
295
 
266
296
  function cloneButtons(
@@ -270,7 +300,43 @@ function cloneButtons(
270
300
  return buttons.map((button) => ({ ...button }));
271
301
  }
272
302
 
273
- function normalizeOption(option: FormOptionInput): FormOption {
303
+ function flattenFormItems(
304
+ rootsInput: readonly FormItem<FormField>[],
305
+ resolveItem: (item: FormItem<FormField>) => FormItem<FormField>
306
+ ): FormItem<FormField>[] {
307
+ const result: FormItem<FormField>[] = [];
308
+ const roots = Array.isArray(rootsInput) ? rootsInput : [];
309
+ const seen = new WeakSet<FormItem<FormField>>();
310
+ const defaultNext = (
311
+ current: FormItem<FormField>
312
+ ): FormItem<FormField> | null => {
313
+ const index = roots.indexOf(current);
314
+ return index >= 0 ? (roots[index + 1] || null) : null;
315
+ };
316
+
317
+ for (const root of roots) {
318
+ if (seen.has(root)) continue;
319
+ let current: FormItem<FormField> | null = root;
320
+ const acients: FormItem<FormField>[] = [];
321
+ let guard = 0;
322
+
323
+ while (current && guard < FORM_ITEM_LIMIT) {
324
+ guard += 1;
325
+ if (seen.has(current)) break;
326
+ seen.add(current);
327
+ result.push(current);
328
+ acients.push(current);
329
+ const next = current.next
330
+ ? current.next(current, acients)
331
+ : defaultNext(current);
332
+ current = next ? resolveItem(next) : null;
333
+ }
334
+ }
335
+
336
+ return result;
337
+ }
338
+
339
+ function normalizeOption(option: FieldOption): FormOption {
274
340
  if (option && typeof option === 'object') return option;
275
341
  return { value: option, text: String(option) };
276
342
  }
@@ -337,13 +403,14 @@ function cloneValidator(validator: unknown): FormValidatorConfig {
337
403
 
338
404
  function normalizeProps(input: FormProps): ResolvedFormProps {
339
405
  const props = resolveProps(input, FORM_PROPS_SCHEMA, 'Form.props');
406
+ const fields = cloneFields(props.fields as readonly FormItem<FormField>[]);
340
407
  return {
341
408
  ...props,
342
409
  id: String(props.id),
343
410
  vertical: Boolean(props.vertical),
344
411
  itemVertical: Boolean(props.itemVertical),
345
412
  style: props.style as FormStyle,
346
- fields: cloneFields(props.fields as readonly FormField[]),
413
+ fields,
347
414
  buttons: cloneButtons(props.buttons as readonly FormButton[]),
348
415
  className: resolveClassNames(props.className),
349
416
  validator: cloneValidator(props.validator),
@@ -352,230 +419,271 @@ function normalizeProps(input: FormProps): ResolvedFormProps {
352
419
  };
353
420
  }
354
421
 
355
- class Form extends Component<ResolvedFormProps, FormState, FormDOM> {
356
- declare state: FormState;
357
- validator: ValidatorInstance | null;
358
- cache: FormCache;
359
-
360
- constructor(input: FormProps = {}) {
361
- const props = normalizeProps(input);
362
- super(props);
363
-
364
- this.dom.fields = new Map();
365
- this.validator = null;
366
- this.cleanup.view = null;
367
-
368
- this.cache = {
369
- initial: this.cloneProps(props),
370
- fieldIds: new Map(),
371
- };
372
-
373
- this.state = createDeepStore({
374
- ...this.cloneProps(props),
375
- submitting: false,
376
- data: null,
377
- });
378
- }
422
+ interface FormActions {
423
+ validate(): boolean;
424
+ reset(): Form;
425
+ collectData(): FormDataRecord;
426
+ requestSubmit(): Form;
427
+ setFields(fields: readonly FormItem<FormField>[]): Form;
428
+ resetFields(): Form;
429
+ }
379
430
 
380
- get root(): HTMLFormElement | null {
381
- return this.dom?.root || null;
382
- }
431
+ export type Form = FunctionalComponent<
432
+ ResolvedFormProps,
433
+ FormState,
434
+ HTMLFormElement,
435
+ FormActions
436
+ >;
383
437
 
384
- set root(value: HTMLFormElement | null) {
385
- this.dom.root = value;
386
- }
438
+ function cloneResolvedProps(props: ResolvedFormProps): ResolvedFormProps {
439
+ return {
440
+ ...props,
441
+ fields: cloneFields(props.fields),
442
+ buttons: cloneButtons(props.buttons),
443
+ className: resolveClassNames(props.className),
444
+ validator: cloneValidator(props.validator),
445
+ };
446
+ }
387
447
 
388
- build(): this {
389
- if (this.runtime.destroyed)
390
- throw new Error('Form.build: instance destroyed');
391
- if (this.cleanup.view) return this;
392
- this.init(this.props);
393
-
394
- const host = document.createElement('div');
395
- this.cleanup.view = createRoot((dispose) => {
396
- const viewDispose = render(() => this.view(), host);
397
- onCleanup(viewDispose);
398
- return dispose;
399
- });
448
+ function autoComplete(type: string): string {
449
+ if (type === 'password') return 'current-password';
450
+ if (type === 'email') return 'email';
451
+ return 'on';
452
+ }
400
453
 
401
- return this;
454
+ function isSelected(
455
+ value: FormField['value'] | undefined,
456
+ optionValue: FormOption['value']
457
+ ): boolean {
458
+ if (Array.isArray(value)) {
459
+ return value
460
+ .map((item) => stringifyFormValue(item))
461
+ .includes(stringifyFormValue(optionValue));
402
462
  }
463
+ return value == optionValue;
464
+ }
403
465
 
404
- view(): HTMLFormElement {
405
- const className = this.state.className;
406
- return jsx('form', {
407
- className: () =>
408
- joinClasses(
409
- this.state.className.form,
410
- this.state.vertical ? className.vertical : className.horizontal,
411
- this.state.itemVertical
412
- ? className.itemVertical
413
- : className.itemHorizontal
414
- ),
415
- id: () => this.state.id,
416
- style: () => this.state.style,
417
- 'data-form': 'root',
418
- 'data-form-layout': () =>
419
- this.state.vertical ? 'vertical' : 'horizontal',
420
- 'data-form-item-layout': () =>
421
- this.state.itemVertical ? 'vertical' : 'horizontal',
422
- ref: (element: HTMLFormElement) => {
423
- const changed = this.root !== element;
424
- this.root = element;
425
- setElementStyle(element, this.state.style);
426
- if (changed || !this.validator) this.syncValidator();
427
- },
428
- onSubmit: (event: Event) => this.handleSubmit(event),
429
- onReset: (event: Event) => this.handleReset(event),
430
- children: () => [
431
- ...this.state.fields.map((field, index) =>
432
- this.fieldView(field, index)
433
- ),
434
- this.buttonsView(),
435
- ],
436
- });
466
+ function isChecked(
467
+ value: FormField['value'] | undefined,
468
+ optionValue: FormOption['value'],
469
+ checked: boolean | undefined
470
+ ): boolean {
471
+ if (checked !== undefined) return checked;
472
+ if (Array.isArray(value)) {
473
+ return value
474
+ .map((item) => stringifyFormValue(item))
475
+ .includes(stringifyFormValue(optionValue));
437
476
  }
438
-
439
- fieldView(field: FormField, index: number): HTMLElement {
440
- const id = this.resolveFieldId(field, index);
441
- const hidden = field.type === 'hidden';
442
- const className = this.state.className;
443
-
444
- return jsx('div', {
445
- className: className.item,
446
- 'data-form-item': field.name || String(index),
447
- style: { display: hidden ? 'none' : '' },
448
- children: [
449
- this.labelView(field, id),
450
- jsx('div', {
451
- className: className.control,
452
- 'data-form-control': field.name || String(index),
453
- children: [
454
- this.controlView(field, id, index),
455
- field.help
456
- ? jsx('div', {
457
- className: 'help-block',
458
- 'data-form-help': field.name || String(index),
459
- children: field.help,
460
- })
461
- : null,
462
- ],
463
- }),
464
- ],
465
- });
477
+ if (
478
+ typeof value !== 'string' &&
479
+ typeof value !== 'boolean' &&
480
+ !(value instanceof File)
481
+ ) {
482
+ return false;
466
483
  }
484
+ return (
485
+ value === true ||
486
+ stringifyFormValue(value) === stringifyFormValue(optionValue)
487
+ );
488
+ }
467
489
 
468
- labelView(field: FormField, id: string): HTMLLabelElement | null {
469
- if (field.label === false || field.label === undefined) return null;
470
- const className = this.state.className;
490
+ function radioNodeListItems(value: RadioNodeList | Element): Element[] {
491
+ if (value instanceof RadioNodeList) return Array.from(value);
492
+ return [value];
493
+ }
471
494
 
472
- return jsx('label', {
473
- className: joinClasses(
474
- className.label,
475
- fieldIsRequired(field, this.state.validator?.rules)
476
- ? className.required
477
- : ''
478
- ),
479
- 'data-form-label': field.name || id,
480
- for: id,
481
- children: field.label,
495
+ export function createForm(input: FormProps = {}): Form {
496
+ const props = normalizeProps(input);
497
+ const initial = cloneResolvedProps(props);
498
+ const state = createDeepStore({
499
+ ...cloneResolvedProps(props),
500
+ submitting: false,
501
+ data: null,
502
+ }) as FormState;
503
+ const cache: FormCache = { initial, fieldIds: new Map() };
504
+ const fieldKeys = new WeakMap<object, string>();
505
+ const [chainVersion, setChainVersion] = createSignal(0);
506
+ let runtimeItems = new WeakMap<FormItem<FormField>, FormItem<FormField>>();
507
+ let validator: ValidatorInstance | null = null;
508
+ let form!: Form;
509
+ const resolveRuntimeItem = (
510
+ item: FormItem<FormField>
511
+ ): FormItem<FormField> => {
512
+ if (state.fields.includes(item)) return item;
513
+ const cached = runtimeItems.get(item);
514
+ if (cached) return cached;
515
+ const runtimeItem = createDeepStore(cloneFormItem(item)) as FormItem<FormField>;
516
+ runtimeItems.set(item, runtimeItem);
517
+ return runtimeItem;
518
+ };
519
+ const renderedItems = createMemo(() => {
520
+ chainVersion();
521
+ return flattenFormItems(state.fields, resolveRuntimeItem);
522
+ });
523
+
524
+ const fieldKey = (field: FormField): string => {
525
+ if (field.id) return `id:${field.id}`;
526
+ if (field.name) return `name:${field.name}`;
527
+ if (!fieldKeys.has(field)) fieldKeys.set(field, randomId());
528
+ return fieldKeys.get(field) as string;
529
+ };
530
+ const itemKey = (item: FormItem, index: number): string => {
531
+ if (item.id) return `item:${item.id}`;
532
+ const field = item.payload as FormField;
533
+ if (field && typeof field === 'object') return `${fieldKey(field)}:${item.type}`;
534
+ return `item:${item.type}:${index}`;
535
+ };
536
+ const resolveFieldId = (field: FormField, index: number): string => {
537
+ if (field.id) return field.id;
538
+ const key = field.name || index;
539
+ if (!cache.fieldIds.has(key)) {
540
+ cache.fieldIds.set(key, `${state.id}_field_${index}`);
541
+ }
542
+ return cache.fieldIds.get(key) as string;
543
+ };
544
+ const syncValidator = (): void => {
545
+ const element = form.element;
546
+ if (!element) return;
547
+ const options = { ...state.validator, onSubmit: null };
548
+ if (validator?.element === element) {
549
+ validator.props = options;
550
+ return;
551
+ }
552
+ validator?.destroy();
553
+ validator = createValidator(element, options, false) as ValidatorInstance;
554
+ };
555
+ const validate = (): boolean => validator?.validate() ?? true;
556
+ const resetValidationState = (): void => {
557
+ const element = form.element;
558
+ if (!element) return;
559
+ for (const control of Array.from(element.elements)) {
560
+ control.classList.remove('is-valid', 'is-invalid');
561
+ }
562
+ for (const help of all<HTMLElement>(
563
+ '[data-form-help], [data-validator-help]',
564
+ element
565
+ )) {
566
+ if (help.dataset.validatorHelp || help.classList.contains('is-invalid')) {
567
+ help.remove();
568
+ }
569
+ }
570
+ };
571
+ const collectData = (): FormDataRecord => {
572
+ const data: FormDataRecord = {};
573
+ if (!form.element) return data;
574
+ for (const [key, value] of new FormData(form.element).entries()) {
575
+ if (Object.hasOwn(data, key)) {
576
+ data[key] = Array.isArray(data[key])
577
+ ? [...data[key], value]
578
+ : [data[key], value];
579
+ } else data[key] = value;
580
+ }
581
+ return data;
582
+ };
583
+ const handleSubmit = async (event: Event): Promise<void> => {
584
+ event.preventDefault();
585
+ if (state.submitting || !validate()) return;
586
+ const data = collectData();
587
+ flushSync(() => {
588
+ state.submitting = true;
589
+ state.data = data;
482
590
  });
483
- }
484
-
485
- controlView(
486
- field: FormField,
487
- id: string,
488
- index: number
489
- ): FormControlElement | HTMLElement | Node[] | null {
490
- switch (field.type) {
491
- case 'textarea':
492
- return this.textareaView(field, id);
493
- case 'select':
494
- return this.selectView(field, id);
495
- case 'radio':
496
- return this.choiceGroupView(field, id, 'radio');
497
- case 'checkbox':
498
- return Array.isArray(field.options)
499
- ? this.choiceGroupView(field, id, 'checkbox')
500
- : this.inputView(field, id);
501
- case 'switch':
502
- return this.switchView(field, id);
503
- case 'custom':
504
- return normalizeRenderableContentNodes(field.content, {
505
- form: this,
506
- field,
507
- index,
591
+ try {
592
+ await Promise.resolve(state.onSubmit?.(data, form));
593
+ } finally {
594
+ if (!form.runtime.destroyed) {
595
+ flushSync(() => {
596
+ state.submitting = false;
508
597
  });
509
- default:
510
- return this.inputView(field, id);
598
+ }
511
599
  }
512
- }
513
-
514
- inputView(field: FormField, id: string): HTMLInputElement {
515
- const type = field.type || 'text';
516
- const props = this.controlProps(field, id, {
517
- type,
518
- className: field.className || this.state.className.input,
519
- autocomplete: field.autocomplete || this.autoComplete(type),
520
- value: field.value ?? '',
521
- });
600
+ };
522
601
 
523
- return jsx('input', {
524
- ...props,
525
- checked: field.checked === undefined ? undefined : !!field.checked,
602
+ const fieldControls = (field: FormField): Element[] => {
603
+ if (!field.name || !form.element) return [];
604
+ const element = form.element.elements.namedItem(field.name);
605
+ if (!element) return [];
606
+ return radioNodeListItems(element).filter((item) => item instanceof Element);
607
+ };
608
+ const syncFieldValue = (
609
+ field: FormField,
610
+ control:
611
+ | HTMLInputElement
612
+ | HTMLSelectElement
613
+ | HTMLTextAreaElement,
614
+ type: FormControlType | 'checkbox' | 'radio' = 'text'
615
+ ): void => {
616
+ flushSync(() => {
617
+ if (control instanceof HTMLSelectElement) {
618
+ field.value = control.multiple
619
+ ? Array.from(control.selectedOptions).map((option) => option.value)
620
+ : control.value;
621
+ return;
622
+ }
623
+ if (control instanceof HTMLTextAreaElement) {
624
+ field.value = control.value;
625
+ return;
626
+ }
627
+ if (type === 'switch') {
628
+ field.checked = control.checked;
629
+ field.value = control.checked ? control.value : '';
630
+ return;
631
+ }
632
+ if (type === 'radio') {
633
+ if (control.checked) field.value = control.value;
634
+ return;
635
+ }
636
+ if (type === 'checkbox') {
637
+ if (Array.isArray(field.options)) {
638
+ field.value = fieldControls(field)
639
+ .filter(
640
+ (item): item is HTMLInputElement =>
641
+ item instanceof HTMLInputElement && item.checked
642
+ )
643
+ .map((item) => item.value);
644
+ return;
645
+ }
646
+ field.checked = control.checked;
647
+ field.value = control.checked ? control.value : '';
648
+ return;
649
+ }
650
+ field.value = control.value;
651
+ if (control.type === 'checkbox') field.checked = control.checked;
526
652
  });
527
- }
528
-
529
- textareaView(field: FormField, id: string): HTMLTextAreaElement {
530
- return jsx(
531
- 'textarea',
532
- this.controlProps(field, id, {
533
- className: field.className || this.state.className.textarea,
534
- autocomplete: field.autocomplete,
535
- value: field.value ?? '',
536
- })
537
- );
538
- }
539
-
540
- selectView(field: FormField, id: string): HTMLSelectElement {
541
- const value = field.value;
542
- return jsx(
543
- 'select',
544
- this.controlProps(field, id, {
545
- className: field.className || this.state.className.select,
546
- autocomplete: field.autocomplete || 'off',
547
- multiple: !!field.multiple,
548
- children: (field.options || []).map((option) => {
549
- const item = normalizeOption(option);
550
- return jsx('option', {
551
- value: item.value ?? '',
552
- disabled: !!item.disabled,
553
- selected: this.isSelected(value, item.value),
554
- children: item.text ?? item.label ?? item.value ?? '',
555
- });
556
- }),
557
- })
558
- );
559
- }
653
+ setChainVersion((version: number) => version + 1);
654
+ };
560
655
 
561
- choiceGroupView(
656
+ const controlProps = <TExtra extends Record<string, unknown>>(
657
+ field: FormField,
658
+ id: string,
659
+ extra: TExtra
660
+ ): TExtra & Record<string, unknown> => ({
661
+ ...extra,
662
+ name: field.name,
663
+ id,
664
+ placeholder: field.placeholder || '',
665
+ required: !!field.required,
666
+ disabled: () => state.submitting || !!field.disabled,
667
+ readonly: !!field.readonly,
668
+ 'data-form-field': field.name || id,
669
+ });
670
+
671
+ const choiceGroupView = (
562
672
  field: FormField,
563
673
  id: string,
564
674
  type: 'checkbox' | 'radio'
565
- ): HTMLElement {
675
+ ): HTMLElement => {
566
676
  const direction = field.vertical ? 'vertical' : 'horizontal';
567
- const classNameConfig = this.state.className;
568
- const className = joinClasses(
569
- type === 'radio' ? classNameConfig.radio : classNameConfig.checkbox,
570
- direction === 'vertical'
571
- ? classNameConfig.choiceVertical
572
- : classNameConfig.choiceHorizontal,
573
- field.group ? classNameConfig.choiceGroup : '',
574
- field.size ? `is-${field.size}` : ''
575
- );
576
-
677
+ const names = state.className;
577
678
  return jsx('div', {
578
- className,
679
+ className: joinClasses(
680
+ type === 'radio' ? names.radio : names.checkbox,
681
+ direction === 'vertical'
682
+ ? names.choiceVertical
683
+ : names.choiceHorizontal,
684
+ field.group ? names.choiceGroup : '',
685
+ field.size ? `is-${field.size}` : ''
686
+ ),
579
687
  'data-form-choice-group': field.name || id,
580
688
  'data-choice-type': type,
581
689
  'data-choice-layout': direction,
@@ -583,7 +691,7 @@ class Form extends Component<ResolvedFormProps, FormState, FormDOM> {
583
691
  const item = normalizeOption(option);
584
692
  const optionId = `${id}_${optionIndex}`;
585
693
  return jsx('label', {
586
- className: type === 'radio' ? classNameConfig.radioLabel : '',
694
+ className: type === 'radio' ? names.radioLabel : '',
587
695
  'data-form-choice': field.name || id,
588
696
  for: optionId,
589
697
  children: [
@@ -594,302 +702,351 @@ class Form extends Component<ResolvedFormProps, FormState, FormDOM> {
594
702
  value: item.value ?? '',
595
703
  checked:
596
704
  type === 'radio'
597
- ? this.isSelected(field.value, item.value)
598
- : this.isChecked(field.value, item.value, item.checked),
599
- disabled: !!item.disabled || !!field.disabled,
705
+ ? isSelected(field.value, item.value)
706
+ : isChecked(field.value, item.value, item.checked),
707
+ disabled: () =>
708
+ state.submitting || !!item.disabled || !!field.disabled,
600
709
  required: !!field.required,
710
+ onChange: (event: Event) => {
711
+ const target = event.currentTarget;
712
+ if (target instanceof HTMLInputElement) {
713
+ syncFieldValue(field, target, type);
714
+ }
715
+ },
601
716
  }),
602
717
  jsx('span', {
603
- className: type === 'radio' ? classNameConfig.radioText : '',
718
+ className: type === 'radio' ? names.radioText : '',
604
719
  'data-form-choice-text': '',
605
720
  children: item.text ?? item.label ?? item.value ?? '',
606
721
  }),
607
722
  ],
608
723
  });
609
724
  }),
610
- });
611
- }
612
-
613
- switchView(field: FormField, id: string): HTMLLabelElement {
614
- const className = this.state.className;
615
- return jsx('label', {
616
- className: joinClasses(
617
- className.switch,
618
- field.variant ? `is-${field.variant}` : className.switchDefault,
619
- field.size ? `is-${field.size}` : className.switchSizeMd
620
- ),
621
- 'data-form-switch': field.name || id,
622
- for: id,
623
- children: [
624
- jsx('input', {
625
- type: 'checkbox',
626
- id,
627
- name: field.name,
628
- value: field.value ?? 'on',
629
- checked: !!field.checked,
630
- disabled: !!field.disabled,
631
- required: !!field.required,
632
- }),
633
- jsx('span', {
634
- className: className.switchSlider,
635
- 'data-form-switch-slider': '',
636
- }),
637
- ],
638
- });
639
- }
640
-
641
- buttonsView(): HTMLElement | null {
642
- if (!this.state.buttons.length) return null;
643
- const className = this.state.className;
644
-
645
- return jsx('div', {
646
- className: className.buttons,
647
- 'data-form-buttons': '',
648
- children: this.state.buttons.map((button) =>
649
- jsx('button', {
650
- type: button.type || 'button',
651
- className: joinClasses(
652
- className.button,
653
- button.theme ? `is-${button.theme}` : '',
654
- button.className || ''
655
- ),
656
- 'data-action': button.action || button.type || 'button',
657
- disabled: () => !!this.state.submitting || !!button.disabled,
658
- children: button.text ?? button.label ?? '',
659
- })
660
- ),
661
- });
662
- }
725
+ }) as HTMLElement;
726
+ };
663
727
 
664
- controlProps<TExtra extends Record<string, unknown>>(
728
+ const controlView = (
729
+ type: FormItemType,
665
730
  field: FormField,
666
731
  id: string,
667
- extra: TExtra
668
- ): TExtra & {
669
- name: string | undefined;
670
- id: string;
671
- placeholder: string;
672
- required: boolean;
673
- disabled: boolean;
674
- readonly: boolean;
675
- 'data-form-field': string;
676
- ref: (element: FormControlElement) => void;
677
- } {
678
- return {
679
- ...extra,
680
- name: field.name,
681
- id,
682
- placeholder: field.placeholder || '',
683
- required: !!field.required,
684
- disabled: !!field.disabled,
685
- readonly: !!field.readonly,
686
- 'data-form-field': field.name || id,
687
- ref: (element) => {
688
- if (field.name) this.dom.fields.set(field.name, element);
689
- },
690
- };
691
- }
692
-
693
- resolveFieldId(field: FormField, index: number): string {
694
- if (field.id) return field.id;
695
-
696
- const key = field.name || index;
697
- if (!this.cache.fieldIds.has(key)) {
698
- this.cache.fieldIds.set(key, `${this.state.id}_field_${index}`);
699
- }
700
- return this.cache.fieldIds.get(key) || `${this.state.id}_field_${index}`;
701
- }
702
-
703
- syncValidator(): void {
704
- if (!this.root) return;
705
- const options = {
706
- ...this.state.validator,
707
- onSubmit: null,
708
- };
709
-
710
- if (this.validator?.dom.root === this.root) {
711
- this.validator.props = options;
712
- return;
713
- }
714
-
715
- this.validator?.destroy();
716
- this.validator = createValidator(
717
- this.root,
718
- options,
719
- false
720
- ) as ValidatorInstance;
721
- }
722
-
723
- validate(): boolean {
724
- if (!this.validator) return true;
725
- return this.validator.validate();
726
- }
727
-
728
- reset(): this {
729
- this.validator?.reset();
730
- flushSync(() => {
731
- this.state.data = null;
732
- });
733
- return this;
734
- }
735
-
736
- async handleSubmit(event: Event): Promise<void> {
737
- event.preventDefault();
738
- if (this.state.submitting) return;
739
- if (!this.validate()) return;
740
-
741
- const data = this.collectData();
742
- flushSync(() => {
743
- this.state.submitting = true;
744
- this.state.data = data;
745
- });
746
-
747
- try {
748
- await Promise.resolve(this.state.onSubmit?.(data, this));
749
- } finally {
750
- if (!this.runtime.destroyed) {
751
- flushSync(() => {
752
- this.state.submitting = false;
753
- });
754
- }
755
- }
756
- }
757
-
758
- handleReset(event: Event): void {
759
- this.resetValidationState();
760
- flushSync(() => {
761
- this.state.data = null;
762
- });
763
- this.state.onReset?.(event, this);
764
- }
765
-
766
- resetValidationState(): void {
767
- if (!this.root) return;
768
- for (const element of Array.from(this.root.elements)) {
769
- element.classList.remove('is-valid');
770
- element.classList.remove('is-invalid');
732
+ index: number,
733
+ item: FormItem<FormField>
734
+ ): RenderableContent<FormControlContext> => {
735
+ const names = state.className;
736
+ if (type === 'textarea') {
737
+ return jsx(
738
+ 'textarea',
739
+ controlProps(field, id, {
740
+ className: field.className || names.textarea,
741
+ autocomplete: field.autocomplete,
742
+ value: field.value ?? '',
743
+ onInput: (event: Event) => {
744
+ const target = event.currentTarget;
745
+ if (target instanceof HTMLTextAreaElement) {
746
+ syncFieldValue(field, target, 'textarea');
747
+ }
748
+ },
749
+ })
750
+ ) as HTMLTextAreaElement;
771
751
  }
772
- for (const help of all<HTMLElement>(
773
- '[data-form-help], [data-validator-help]',
774
- this.root
775
- )) {
776
- if (help.dataset.validatorHelp || help.classList.contains('is-invalid')) {
777
- help.remove();
778
- }
752
+ if (type === 'select') {
753
+ return jsx(
754
+ 'select',
755
+ controlProps(field, id, {
756
+ className: field.className || names.select,
757
+ autocomplete: field.autocomplete || 'off',
758
+ multiple: !!field.multiple,
759
+ onChange: (event: Event) => {
760
+ const target = event.currentTarget;
761
+ if (target instanceof HTMLSelectElement) {
762
+ syncFieldValue(field, target, 'select');
763
+ }
764
+ },
765
+ children: (field.options || []).map((option) => {
766
+ const item = normalizeOption(option);
767
+ return jsx('option', {
768
+ value: item.value ?? '',
769
+ disabled: !!item.disabled,
770
+ selected: isSelected(field.value, item.value),
771
+ children: item.text ?? item.label ?? item.value ?? '',
772
+ });
773
+ }),
774
+ })
775
+ ) as HTMLSelectElement;
779
776
  }
780
- }
781
-
782
- collectData(): FormDataRecord {
783
- if (!this.root) return {};
784
- return this.collectFormData(this.root);
785
- }
786
-
787
- collectFormData(form: HTMLFormElement): FormDataRecord {
788
- const data: FormDataRecord = {};
789
- const formData = new FormData(form);
790
-
791
- for (const [key, value] of formData.entries()) {
792
- if (Object.hasOwn(data, key)) {
793
- data[key] = Array.isArray(data[key])
794
- ? [...data[key], value]
795
- : [data[key], value];
796
- } else {
797
- data[key] = value;
798
- }
777
+ if (type === 'radio') return choiceGroupView(field, id, 'radio');
778
+ if (type === 'checkbox' && Array.isArray(field.options)) {
779
+ return choiceGroupView(field, id, 'checkbox');
799
780
  }
800
-
801
- return data;
802
- }
803
-
804
- requestSubmit(): this {
805
- if (!this.root) return this;
806
- if (typeof this.root.requestSubmit === 'function') {
807
- this.root.requestSubmit();
808
- return this;
781
+ if (type === 'switch') {
782
+ return jsx('label', {
783
+ className: joinClasses(
784
+ names.switch,
785
+ field.variant ? `is-${field.variant}` : names.switchDefault,
786
+ field.size ? `is-${field.size}` : names.switchSizeMd
787
+ ),
788
+ 'data-form-switch': field.name || id,
789
+ for: id,
790
+ children: [
791
+ jsx('input', {
792
+ type: 'checkbox',
793
+ id,
794
+ name: field.name,
795
+ value: field.value ?? '1',
796
+ checked: !!field.checked,
797
+ disabled: () => state.submitting || !!field.disabled,
798
+ required: !!field.required,
799
+ onChange: (event: Event) => {
800
+ const target = event.currentTarget;
801
+ if (target instanceof HTMLInputElement) {
802
+ syncFieldValue(field, target, 'switch');
803
+ }
804
+ },
805
+ }),
806
+ jsx('span', {
807
+ className: names.switchSlider,
808
+ 'data-form-switch-slider': '',
809
+ }),
810
+ ],
811
+ }) as HTMLLabelElement;
809
812
  }
810
-
811
- const event = new Event('submit', { bubbles: true, cancelable: true });
812
- this.root.dispatchEvent(event);
813
- return this;
814
- }
815
-
816
- setFields(fields: readonly FormField[]): this {
817
- validateParam('fields', fields, 'array', 'Form.setFields');
818
- this.cache.fieldIds.clear();
819
- flushSync(() => {
820
- this.state.fields = cloneFields(fields);
821
- });
822
- return this;
823
- }
824
-
825
- resetFields(): this {
826
- return this.setFields(this.cache.initial.fields);
827
- }
828
-
829
- cloneProps(props: ResolvedFormProps): ResolvedFormProps {
830
- return {
831
- ...props,
832
- fields: cloneFields(props.fields),
833
- buttons: cloneButtons(props.buttons),
834
- className: resolveClassNames(props.className),
835
- validator: cloneValidator(props.validator),
836
- };
837
- }
838
-
839
- autoComplete(type: string): string {
840
- switch (type) {
841
- case 'password':
842
- return 'current-password';
843
- case 'email':
844
- return 'email';
845
- default:
846
- return 'on';
813
+ if (type === 'custom') {
814
+ return typeof field.content === 'function'
815
+ ? field.content({ form, field, index, item })
816
+ : field.content;
847
817
  }
848
- }
818
+ const inputType = type || 'text';
819
+ return jsx('input', {
820
+ ...controlProps(field, id, {
821
+ type: inputType,
822
+ className: field.className || names.input,
823
+ autocomplete: field.autocomplete || autoComplete(inputType),
824
+ value: field.value ?? '',
825
+ onInput: (event: Event) => {
826
+ const target = event.currentTarget;
827
+ if (target instanceof HTMLInputElement) {
828
+ syncFieldValue(field, target, inputType);
829
+ }
830
+ },
831
+ }),
832
+ checked: field.checked === undefined ? undefined : !!field.checked,
833
+ }) as HTMLInputElement;
834
+ };
849
835
 
850
- isSelected(
851
- value: FormField['value'] | undefined,
852
- optionValue: FormOption['value']
853
- ): boolean {
854
- if (Array.isArray(value))
855
- return value
856
- .map((item) => stringifyFormValue(item))
857
- .includes(stringifyFormValue(optionValue));
858
- return value == optionValue;
859
- }
836
+ const fieldView = (
837
+ itemAccessor: () => FormItem<FormField>,
838
+ indexAccessor: () => number
839
+ ): HTMLElement => {
840
+ const item = itemAccessor();
841
+ const field = item.payload;
842
+ const fieldAccessor = (): FormField => itemAccessor().payload;
843
+ const index = indexAccessor();
844
+ const id = resolveFieldId(field, index);
845
+ return jsx('div', {
846
+ className: () => state.className.item,
847
+ 'data-form-item': () => fieldAccessor().name || String(indexAccessor()),
848
+ style: () => ({
849
+ display: itemAccessor().type === 'hidden' ? 'none' : '',
850
+ }),
851
+ children: [
852
+ () => {
853
+ const current = fieldAccessor();
854
+ if (current.label === false || current.label === undefined) {
855
+ return null;
856
+ }
857
+ return jsx('label', {
858
+ className: () =>
859
+ joinClasses(
860
+ state.className.label,
861
+ fieldIsRequired(fieldAccessor(), state.validator.rules)
862
+ ? state.className.required
863
+ : ''
864
+ ),
865
+ 'data-form-label': () => fieldAccessor().name || id,
866
+ for: id,
867
+ children: () => fieldAccessor().label,
868
+ });
869
+ },
870
+ jsx('div', {
871
+ className: () => state.className.control,
872
+ 'data-form-control': () =>
873
+ fieldAccessor().name || String(indexAccessor()),
874
+ children: [
875
+ controlView(item.type, field, id, index, item),
876
+ () => {
877
+ const current = fieldAccessor();
878
+ return current.help
879
+ ? jsx('div', {
880
+ className: 'help-block',
881
+ 'data-form-help': () =>
882
+ fieldAccessor().name || String(indexAccessor()),
883
+ children: () => fieldAccessor().help,
884
+ })
885
+ : null;
886
+ },
887
+ ],
888
+ }),
889
+ ],
890
+ }) as HTMLElement;
891
+ };
860
892
 
861
- isChecked(
862
- value: FormField['value'] | undefined,
863
- optionValue: FormOption['value'],
864
- checked: boolean | undefined
865
- ): boolean {
866
- if (checked !== undefined) return !!checked;
867
- if (Array.isArray(value))
868
- return value
869
- .map((item) => stringifyFormValue(item))
870
- .includes(stringifyFormValue(optionValue));
871
- if (
872
- typeof value !== 'string' &&
873
- typeof value !== 'boolean' &&
874
- !(value instanceof File)
875
- ) {
876
- return false;
877
- }
878
- return (
879
- value === true ||
880
- stringifyFormValue(value) === stringifyFormValue(optionValue)
881
- );
882
- }
893
+ const itemView = (
894
+ itemAccessor: () => FormItem<FormField>,
895
+ indexAccessor: () => number
896
+ ): HTMLElement => fieldView(itemAccessor, indexAccessor);
883
897
 
884
- protected onDestroy(): void {
885
- const cleanupView = this.cleanup.view;
886
- if (typeof cleanupView === 'function') cleanupView();
887
- this.cleanup.view = null;
888
- this.validator?.destroy();
889
- this.validator = null;
890
- }
891
- }
898
+ const buttonsView = (): HTMLElement =>
899
+ jsx('div', {
900
+ className: () => state.className.buttons,
901
+ 'data-form-buttons': '',
902
+ hidden: () => state.buttons.length === 0,
903
+ children: For({
904
+ each: () => state.buttons,
905
+ key: (button: FormButton, index: number) =>
906
+ `${button.action || button.type || 'button'}:${index}`,
907
+ children: (buttonAccessor: () => FormButton) =>
908
+ jsx('button', {
909
+ type: () => buttonAccessor().type || 'button',
910
+ className: () => {
911
+ const button = buttonAccessor();
912
+ return joinClasses(
913
+ state.className.button,
914
+ button.theme ? `is-${button.theme}` : '',
915
+ button.className || ''
916
+ );
917
+ },
918
+ 'data-action': () => {
919
+ const button = buttonAccessor();
920
+ return button.action || button.type || 'button';
921
+ },
922
+ disabled: () => state.submitting || !!buttonAccessor().disabled,
923
+ children: () => {
924
+ const button = buttonAccessor();
925
+ const content = button.text ?? button.label ?? '';
926
+ const submitting =
927
+ state.submitting &&
928
+ (button.type === 'submit' || button.action === 'submit');
929
+ return submitting ? [createLoading(), content] : content;
930
+ },
931
+ }),
932
+ }),
933
+ }) as HTMLElement;
934
+
935
+ form = defineComponent<
936
+ ResolvedFormProps,
937
+ FormState,
938
+ HTMLFormElement,
939
+ FormActions
940
+ >({
941
+ name: 'Form',
942
+ props,
943
+ state,
944
+ actions: {
945
+ validate,
946
+ reset() {
947
+ validator?.reset();
948
+ flushSync(() => {
949
+ state.data = null;
950
+ });
951
+ return form;
952
+ },
953
+ collectData,
954
+ requestSubmit() {
955
+ const element = form.element;
956
+ if (!element) return form;
957
+ if (typeof element.requestSubmit === 'function')
958
+ element.requestSubmit();
959
+ else
960
+ element.dispatchEvent(
961
+ new Event('submit', { bubbles: true, cancelable: true })
962
+ );
963
+ return form;
964
+ },
965
+ setFields(fields) {
966
+ validateParam('fields', fields, 'array', 'Form.setFields');
967
+ cache.fieldIds.clear();
968
+ runtimeItems = new WeakMap();
969
+ flushSync(() => {
970
+ state.fields = cloneFields(fields);
971
+ });
972
+ return form;
973
+ },
974
+ resetFields() {
975
+ cache.fieldIds.clear();
976
+ runtimeItems = new WeakMap();
977
+ flushSync(() => {
978
+ state.fields = cloneFields(initial.fields);
979
+ });
980
+ return form;
981
+ },
982
+ },
983
+ normalizeStatePatch(patch) {
984
+ return {
985
+ ...patch,
986
+ ...(Object.hasOwn(patch, 'fields')
987
+ ? { fields: cloneFields(patch.fields) }
988
+ : {}),
989
+ ...(Object.hasOwn(patch, 'buttons')
990
+ ? { buttons: cloneButtons(patch.buttons) }
991
+ : {}),
992
+ ...(Object.hasOwn(patch, 'className')
993
+ ? { className: resolveClassNames(patch.className) }
994
+ : {}),
995
+ ...(Object.hasOwn(patch, 'validator')
996
+ ? { validator: cloneValidator(patch.validator) }
997
+ : {}),
998
+ };
999
+ },
1000
+ view: () => {
1001
+ const element = jsx('form', {
1002
+ className: () =>
1003
+ joinClasses(
1004
+ state.className.form,
1005
+ state.vertical
1006
+ ? state.className.vertical
1007
+ : state.className.horizontal,
1008
+ state.itemVertical
1009
+ ? state.className.itemVertical
1010
+ : state.className.itemHorizontal
1011
+ ),
1012
+ id: () => state.id,
1013
+ 'data-form': 'root',
1014
+ 'data-form-layout': () => (state.vertical ? 'vertical' : 'horizontal'),
1015
+ 'data-form-item-layout': () =>
1016
+ state.itemVertical ? 'vertical' : 'horizontal',
1017
+ onSubmit: (event: Event) => void handleSubmit(event),
1018
+ onReset: (event: Event) => {
1019
+ resetValidationState();
1020
+ flushSync(() => {
1021
+ state.data = null;
1022
+ });
1023
+ state.onReset?.(event, form);
1024
+ },
1025
+ children: [
1026
+ For({
1027
+ each: renderedItems,
1028
+ key: itemKey,
1029
+ children: itemView,
1030
+ }),
1031
+ buttonsView(),
1032
+ ],
1033
+ }) as HTMLFormElement;
1034
+ createEffect(() => setElementStyle(element, state.style));
1035
+ createEffect(() => {
1036
+ if (validator) validator.props = { ...state.validator, onSubmit: null };
1037
+ });
1038
+ return element;
1039
+ },
1040
+ onBuild() {
1041
+ syncValidator();
1042
+ },
1043
+ onDestroy() {
1044
+ validator?.destroy();
1045
+ validator = null;
1046
+ cache.fieldIds.clear();
1047
+ runtimeItems = new WeakMap();
1048
+ },
1049
+ }) as Form;
892
1050
 
893
- export function createForm(props: FormProps = {}): Form {
894
- return new Form(props);
1051
+ return form;
895
1052
  }