dalila 1.5.13 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +47 -0
  2. package/dist/componentes/ui/accordion/index.d.ts +2 -0
  3. package/dist/componentes/ui/accordion/index.js +114 -0
  4. package/dist/componentes/ui/calendar/index.d.ts +2 -0
  5. package/dist/componentes/ui/calendar/index.js +132 -0
  6. package/dist/componentes/ui/combobox/index.d.ts +2 -0
  7. package/dist/componentes/ui/combobox/index.js +161 -0
  8. package/dist/componentes/ui/dialog/index.d.ts +10 -0
  9. package/dist/componentes/ui/dialog/index.js +54 -0
  10. package/dist/componentes/ui/drawer/index.d.ts +2 -0
  11. package/dist/componentes/ui/drawer/index.js +41 -0
  12. package/dist/componentes/ui/dropdown/index.d.ts +2 -0
  13. package/dist/componentes/ui/dropdown/index.js +48 -0
  14. package/dist/componentes/ui/dropzone/index.d.ts +2 -0
  15. package/dist/componentes/ui/dropzone/index.js +92 -0
  16. package/dist/componentes/ui/env.d.ts +1 -0
  17. package/dist/componentes/ui/env.js +2 -0
  18. package/dist/componentes/ui/index.d.ts +13 -0
  19. package/dist/componentes/ui/index.js +12 -0
  20. package/dist/componentes/ui/popover/index.d.ts +2 -0
  21. package/dist/componentes/ui/popover/index.js +156 -0
  22. package/dist/componentes/ui/runtime.d.ts +20 -0
  23. package/dist/componentes/ui/runtime.js +421 -0
  24. package/dist/componentes/ui/tabs/index.d.ts +3 -0
  25. package/dist/componentes/ui/tabs/index.js +101 -0
  26. package/dist/componentes/ui/toast/index.d.ts +3 -0
  27. package/dist/componentes/ui/toast/index.js +115 -0
  28. package/dist/componentes/ui/ui-types.d.ts +175 -0
  29. package/dist/componentes/ui/ui-types.js +1 -0
  30. package/dist/componentes/ui/validate.d.ts +7 -0
  31. package/dist/componentes/ui/validate.js +71 -0
  32. package/dist/components/ui/accordion/index.d.ts +2 -0
  33. package/dist/components/ui/accordion/index.js +114 -0
  34. package/dist/components/ui/calendar/index.d.ts +2 -0
  35. package/dist/components/ui/calendar/index.js +132 -0
  36. package/dist/components/ui/combobox/index.d.ts +2 -0
  37. package/dist/components/ui/combobox/index.js +161 -0
  38. package/dist/components/ui/dialog/index.d.ts +10 -0
  39. package/dist/components/ui/dialog/index.js +54 -0
  40. package/dist/components/ui/drawer/index.d.ts +2 -0
  41. package/dist/components/ui/drawer/index.js +41 -0
  42. package/dist/components/ui/dropdown/index.d.ts +2 -0
  43. package/dist/components/ui/dropdown/index.js +48 -0
  44. package/dist/components/ui/dropzone/index.d.ts +2 -0
  45. package/dist/components/ui/dropzone/index.js +92 -0
  46. package/dist/components/ui/env.d.ts +1 -0
  47. package/dist/components/ui/env.js +2 -0
  48. package/dist/components/ui/index.d.ts +13 -0
  49. package/dist/components/ui/index.js +12 -0
  50. package/dist/components/ui/popover/index.d.ts +2 -0
  51. package/dist/components/ui/popover/index.js +156 -0
  52. package/dist/components/ui/runtime.d.ts +20 -0
  53. package/dist/components/ui/runtime.js +421 -0
  54. package/dist/components/ui/tabs/index.d.ts +3 -0
  55. package/dist/components/ui/tabs/index.js +101 -0
  56. package/dist/components/ui/toast/index.d.ts +3 -0
  57. package/dist/components/ui/toast/index.js +115 -0
  58. package/dist/components/ui/ui-types.d.ts +175 -0
  59. package/dist/components/ui/ui-types.js +1 -0
  60. package/dist/components/ui/validate.d.ts +7 -0
  61. package/dist/components/ui/validate.js +71 -0
  62. package/dist/form/form-types.d.ts +181 -0
  63. package/dist/form/form-types.js +4 -0
  64. package/dist/form/form.d.ts +71 -0
  65. package/dist/form/form.js +1073 -0
  66. package/dist/form/index.d.ts +2 -0
  67. package/dist/form/index.js +2 -0
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +1 -0
  70. package/dist/runtime/bind.js +567 -9
  71. package/dist/ui/accordion.d.ts +2 -0
  72. package/dist/ui/accordion.js +114 -0
  73. package/dist/ui/calendar.d.ts +2 -0
  74. package/dist/ui/calendar.js +132 -0
  75. package/dist/ui/combobox.d.ts +2 -0
  76. package/dist/ui/combobox.js +161 -0
  77. package/dist/ui/dialog.d.ts +10 -0
  78. package/dist/ui/dialog.js +54 -0
  79. package/dist/ui/drawer.d.ts +2 -0
  80. package/dist/ui/drawer.js +41 -0
  81. package/dist/ui/dropdown.d.ts +2 -0
  82. package/dist/ui/dropdown.js +48 -0
  83. package/dist/ui/dropzone.d.ts +2 -0
  84. package/dist/ui/dropzone.js +92 -0
  85. package/dist/ui/env.d.ts +1 -0
  86. package/dist/ui/env.js +2 -0
  87. package/dist/ui/index.d.ts +13 -0
  88. package/dist/ui/index.js +12 -0
  89. package/dist/ui/popover.d.ts +2 -0
  90. package/dist/ui/popover.js +156 -0
  91. package/dist/ui/runtime.d.ts +20 -0
  92. package/dist/ui/runtime.js +421 -0
  93. package/dist/ui/tabs.d.ts +3 -0
  94. package/dist/ui/tabs.js +101 -0
  95. package/dist/ui/toast.d.ts +3 -0
  96. package/dist/ui/toast.js +115 -0
  97. package/dist/ui/ui-types.d.ts +175 -0
  98. package/dist/ui/ui-types.js +1 -0
  99. package/dist/ui/validate.d.ts +7 -0
  100. package/dist/ui/validate.js +71 -0
  101. package/package.json +60 -2
  102. package/src/components/ui/accordion/accordion.css +90 -0
  103. package/src/components/ui/alert/alert.css +78 -0
  104. package/src/components/ui/avatar/avatar.css +45 -0
  105. package/src/components/ui/badge/badge.css +71 -0
  106. package/src/components/ui/breadcrumb/breadcrumb.css +41 -0
  107. package/src/components/ui/button/button.css +135 -0
  108. package/src/components/ui/calendar/calendar.css +96 -0
  109. package/src/components/ui/card/card.css +93 -0
  110. package/src/components/ui/checkbox/checkbox.css +57 -0
  111. package/src/components/ui/chip/chip.css +62 -0
  112. package/src/components/ui/collapsible/collapsible.css +61 -0
  113. package/src/components/ui/combobox/combobox.css +85 -0
  114. package/src/components/ui/dalila/dalila.css +42 -0
  115. package/src/components/ui/dalila-core/dalila-core.css +14 -0
  116. package/src/components/ui/dialog/dialog.css +125 -0
  117. package/src/components/ui/drawer/drawer.css +122 -0
  118. package/src/components/ui/dropdown/dropdown.css +87 -0
  119. package/src/components/ui/dropzone/dropzone.css +47 -0
  120. package/src/components/ui/empty-state/empty-state.css +33 -0
  121. package/src/components/ui/form/form.css +44 -0
  122. package/src/components/ui/input/input.css +106 -0
  123. package/src/components/ui/layout/layout.css +62 -0
  124. package/src/components/ui/pagination/pagination.css +55 -0
  125. package/src/components/ui/popover/popover.css +55 -0
  126. package/src/components/ui/radio/radio.css +56 -0
  127. package/src/components/ui/separator/separator.css +38 -0
  128. package/src/components/ui/skeleton/skeleton.css +57 -0
  129. package/src/components/ui/slider/slider.css +60 -0
  130. package/src/components/ui/spinner/spinner.css +38 -0
  131. package/src/components/ui/table/table.css +54 -0
  132. package/src/components/ui/tabs/tabs.css +74 -0
  133. package/src/components/ui/toast/toast.css +100 -0
  134. package/src/components/ui/toggle/toggle.css +90 -0
  135. package/src/components/ui/tokens/tokens.css +161 -0
  136. package/src/components/ui/tooltip/tooltip.css +53 -0
  137. package/src/components/ui/typography/typography.css +81 -0
@@ -0,0 +1,2 @@
1
+ export * from "./form-types.js";
2
+ export { createForm, parseFormData, WRAPPED_HANDLER } from "./form.js";
@@ -0,0 +1,2 @@
1
+ export * from "./form-types.js";
2
+ export { createForm, parseFormData, WRAPPED_HANDLER } from "./form.js";
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./core/index.js";
2
2
  export * from "./context/index.js";
3
3
  export * from "./router/index.js";
4
4
  export * from "./runtime/index.js";
5
+ export * from "./form/index.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from "./core/index.js";
2
2
  export * from "./context/index.js";
3
3
  export * from "./router/index.js";
4
4
  export * from "./runtime/index.js";
5
+ export * from "./form/index.js";
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * @module dalila/runtime
8
8
  */
9
- import { effect, createScope, withScope, isInDevMode } from '../core/index.js';
9
+ import { effect, createScope, withScope, isInDevMode, signal } from '../core/index.js';
10
+ import { WRAPPED_HANDLER } from '../form/index.js';
10
11
  // ============================================================================
11
12
  // Utilities
12
13
  // ============================================================================
@@ -485,6 +486,554 @@ function bindAttrs(root, ctx, cleanups) {
485
486
  }
486
487
  }
487
488
  // ============================================================================
489
+ // Form Directives
490
+ // ============================================================================
491
+ /**
492
+ * Bind all [d-form] directives within root.
493
+ * Associates a form element with a Form instance from the context.
494
+ * Also auto-wraps d-on-submit handlers through form.handleSubmit().
495
+ */
496
+ function bindForm(root, ctx, cleanups) {
497
+ const elements = qsaIncludingRoot(root, '[d-form]');
498
+ for (const el of elements) {
499
+ // Skip forms inside d-each templates
500
+ // They'll be bound when the template is cloned and bound individually
501
+ if (el.closest('[d-each]')) {
502
+ continue;
503
+ }
504
+ if (!(el instanceof HTMLFormElement)) {
505
+ warn('d-form: must be used on a <form> element');
506
+ continue;
507
+ }
508
+ const bindingName = normalizeBinding(el.getAttribute('d-form'));
509
+ if (!bindingName)
510
+ continue;
511
+ const form = ctx[bindingName];
512
+ if (!form || typeof form !== 'object' || !('handleSubmit' in form)) {
513
+ warn(`d-form: "${bindingName}" is not a valid Form instance`);
514
+ continue;
515
+ }
516
+ // Register form element with the Form instance
517
+ if ('_setFormElement' in form && typeof form._setFormElement === 'function') {
518
+ form._setFormElement(el);
519
+ }
520
+ // Auto-wrap d-on-submit handler through form.handleSubmit()
521
+ // Don't mutate shared ctx - add listener directly to this form element
522
+ const submitHandlerName = normalizeBinding(el.getAttribute('d-on-submit'));
523
+ if (submitHandlerName) {
524
+ const originalHandler = ctx[submitHandlerName];
525
+ if (typeof originalHandler === 'function') {
526
+ // Check if handler is already wrapped to avoid double-wrapping
527
+ // If user did: const save = form.handleSubmit(...), don't wrap again
528
+ const isAlreadyWrapped = originalHandler[WRAPPED_HANDLER] === true;
529
+ const finalHandler = isAlreadyWrapped
530
+ ? originalHandler
531
+ : form.handleSubmit(originalHandler);
532
+ // Add submit listener directly to form element (not via d-on-submit)
533
+ // This avoids mutating the shared context
534
+ el.addEventListener('submit', finalHandler);
535
+ // Remove d-on-submit to prevent bindEvents from adding duplicate listener
536
+ el.removeAttribute('d-on-submit');
537
+ // Restore attribute on cleanup so dispose()+bind() (HMR) can rediscover it
538
+ cleanups.push(() => {
539
+ el.removeEventListener('submit', finalHandler);
540
+ el.setAttribute('d-on-submit', submitHandlerName);
541
+ });
542
+ }
543
+ }
544
+ }
545
+ }
546
+ /**
547
+ * Bind all [d-field] directives within root.
548
+ * Registers field elements with their Form instance and sets up a11y attributes.
549
+ */
550
+ function bindField(root, ctx, cleanups) {
551
+ const elements = qsaIncludingRoot(root, '[d-field]');
552
+ for (const el of elements) {
553
+ // Prefer data-field-path (set by d-array) over d-field for full path
554
+ const dataFieldPath = el.getAttribute('data-field-path');
555
+ const dFieldPath = normalizeBinding(el.getAttribute('d-field'));
556
+ const fieldPath = dataFieldPath || dFieldPath;
557
+ if (!fieldPath)
558
+ continue;
559
+ // Find the form element - use context first (for detached clones), then closest()
560
+ // When bind() runs on d-array clones, the clone is still detached from DOM,
561
+ // so el.closest('form[d-form]') returns null. We pass form refs through context.
562
+ const formEl = ctx._formElement || el.closest('form[d-form]');
563
+ if (!formEl) {
564
+ warn(`d-field: field "${fieldPath}" must be inside a [d-form]`);
565
+ continue;
566
+ }
567
+ const formBinding = ctx._formBinding || normalizeBinding(formEl.getAttribute('d-form'));
568
+ if (!formBinding)
569
+ continue;
570
+ const form = ctx[formBinding];
571
+ if (!form || typeof form !== 'object')
572
+ continue;
573
+ const htmlEl = el;
574
+ // Set name attribute if not already set (use full path)
575
+ if (!htmlEl.getAttribute('name')) {
576
+ htmlEl.setAttribute('name', fieldPath);
577
+ }
578
+ // Register field with form using full path
579
+ if ('_registerField' in form && typeof form._registerField === 'function') {
580
+ const unregister = form._registerField(fieldPath, htmlEl);
581
+ cleanups.push(unregister);
582
+ }
583
+ // Setup reactive aria-invalid based on error state
584
+ if ('error' in form && typeof form.error === 'function') {
585
+ effect(() => {
586
+ // Read current path from DOM attribute inside effect
587
+ // This allows the effect to see updated paths after array reorder
588
+ const currentPath = htmlEl.getAttribute('data-field-path') || htmlEl.getAttribute('name') || fieldPath;
589
+ const errorMsg = form.error(currentPath);
590
+ if (errorMsg) {
591
+ htmlEl.setAttribute('aria-invalid', 'true');
592
+ // Use form prefix for unique IDs across multiple forms
593
+ const errorId = `${formBinding}_${currentPath.replace(/[.\[\]]/g, '_')}_error`;
594
+ htmlEl.setAttribute('aria-describedby', errorId);
595
+ }
596
+ else {
597
+ htmlEl.removeAttribute('aria-invalid');
598
+ htmlEl.removeAttribute('aria-describedby');
599
+ }
600
+ });
601
+ }
602
+ }
603
+ }
604
+ /**
605
+ * Bind all [d-error] directives within root.
606
+ * Displays error messages for specific fields.
607
+ */
608
+ function bindError(root, ctx, cleanups) {
609
+ const elements = qsaIncludingRoot(root, '[d-error]');
610
+ for (const el of elements) {
611
+ // Prefer data-error-path (set by d-array) over d-error for full path
612
+ const dataErrorPath = el.getAttribute('data-error-path');
613
+ const dErrorPath = normalizeBinding(el.getAttribute('d-error'));
614
+ const fieldPath = dataErrorPath || dErrorPath;
615
+ if (!fieldPath)
616
+ continue;
617
+ // Find the form element - use context first (for detached clones), then closest()
618
+ // When bind() runs on d-array clones, the clone is still detached from DOM,
619
+ // so el.closest('form[d-form]') returns null. We pass form refs through context.
620
+ const formEl = ctx._formElement || el.closest('form[d-form]');
621
+ if (!formEl) {
622
+ warn(`d-error: error for "${fieldPath}" must be inside a [d-form]`);
623
+ continue;
624
+ }
625
+ const formBinding = ctx._formBinding || normalizeBinding(formEl.getAttribute('d-form'));
626
+ if (!formBinding)
627
+ continue;
628
+ const form = ctx[formBinding];
629
+ if (!form || typeof form !== 'object' || !('error' in form))
630
+ continue;
631
+ const htmlEl = el;
632
+ // Generate stable ID with form prefix to avoid duplicate IDs
633
+ // Multiple forms on same page can have fields with same names
634
+ const errorId = `${formBinding}_${fieldPath.replace(/[.\[\]]/g, '_')}_error`;
635
+ htmlEl.id = errorId;
636
+ // Set role for accessibility
637
+ htmlEl.setAttribute('role', 'alert');
638
+ htmlEl.setAttribute('aria-live', 'polite');
639
+ // Reactive error display
640
+ effect(() => {
641
+ // Read current path from DOM attribute inside effect
642
+ // This allows the effect to see updated paths after array reorder
643
+ const currentPath = htmlEl.getAttribute('data-error-path') || fieldPath;
644
+ const errorMsg = form.error(currentPath);
645
+ if (errorMsg) {
646
+ // Update ID to match current path (with form prefix for uniqueness)
647
+ const errorId = `${formBinding}_${currentPath.replace(/[.\[\]]/g, '_')}_error`;
648
+ htmlEl.id = errorId;
649
+ htmlEl.textContent = errorMsg;
650
+ htmlEl.style.display = '';
651
+ }
652
+ else {
653
+ htmlEl.textContent = '';
654
+ htmlEl.style.display = 'none';
655
+ }
656
+ });
657
+ }
658
+ }
659
+ /**
660
+ * Bind all [d-form-error] directives within root.
661
+ * Displays form-level error messages.
662
+ */
663
+ function bindFormError(root, ctx, cleanups) {
664
+ const elements = qsaIncludingRoot(root, '[d-form-error]');
665
+ for (const el of elements) {
666
+ // Use the attribute value as explicit form binding name when provided
667
+ const explicitBinding = normalizeBinding(el.getAttribute('d-form-error'));
668
+ // Fall back to finding the form element via context or closest()
669
+ const formEl = ctx._formElement || el.closest('form[d-form]');
670
+ const formBinding = explicitBinding
671
+ || ctx._formBinding
672
+ || (formEl ? normalizeBinding(formEl.getAttribute('d-form')) : null);
673
+ if (!formBinding) {
674
+ warn('d-form-error: must specify a form binding or be inside a [d-form]');
675
+ continue;
676
+ }
677
+ const form = ctx[formBinding];
678
+ if (!form || typeof form !== 'object' || !('formError' in form))
679
+ continue;
680
+ const htmlEl = el;
681
+ // Set role for accessibility
682
+ htmlEl.setAttribute('role', 'alert');
683
+ htmlEl.setAttribute('aria-live', 'polite');
684
+ // Reactive form error display
685
+ effect(() => {
686
+ const errorMsg = form.formError();
687
+ if (errorMsg) {
688
+ htmlEl.textContent = errorMsg;
689
+ htmlEl.style.display = '';
690
+ }
691
+ else {
692
+ htmlEl.textContent = '';
693
+ htmlEl.style.display = 'none';
694
+ }
695
+ });
696
+ }
697
+ }
698
+ /**
699
+ * Bind all [d-array] directives within root.
700
+ * Renders field arrays with stable keys for reordering.
701
+ * Preserves DOM state by reusing keyed nodes instead of full teardown.
702
+ */
703
+ function bindArray(root, ctx, cleanups) {
704
+ const elements = qsaIncludingRoot(root, '[d-array]')
705
+ // Skip d-array inside d-each templates
706
+ // They'll be bound when the template is cloned
707
+ // Note: qsaIncludingRoot's boundary logic already prevents duplicate processing,
708
+ // so we don't need an additional filter for nested d-arrays
709
+ .filter(el => !el.closest('[d-each]'));
710
+ for (const el of elements) {
711
+ // Prefer data-array-path (set by parent d-array for nested arrays) over d-array
712
+ const dataArrayPath = el.getAttribute('data-array-path');
713
+ const dArrayAttr = normalizeBinding(el.getAttribute('d-array'));
714
+ const arrayPath = dataArrayPath || dArrayAttr;
715
+ if (!arrayPath)
716
+ continue;
717
+ // Find the form element — use context first (for detached clones), then closest()
718
+ const formEl = ctx._formElement || el.closest('form[d-form]');
719
+ if (!formEl) {
720
+ warn(`d-array: array "${arrayPath}" must be inside a [d-form]`);
721
+ continue;
722
+ }
723
+ const formBinding = ctx._formBinding || normalizeBinding(formEl.getAttribute('d-form'));
724
+ if (!formBinding)
725
+ continue;
726
+ const form = ctx[formBinding];
727
+ if (!form || typeof form !== 'object' || !('fieldArray' in form))
728
+ continue;
729
+ // Get or create the field array
730
+ const fieldArray = form.fieldArray(arrayPath);
731
+ // Find the template element (d-each inside d-array)
732
+ const templateElement = el.querySelector('[d-each]');
733
+ if (!templateElement) {
734
+ warn(`d-array: array "${arrayPath}" must contain a [d-each] template`);
735
+ continue;
736
+ }
737
+ // Store template reference for closure (TypeScript assertion)
738
+ const template = templateElement;
739
+ const comment = document.createComment(`d-array:${arrayPath}`);
740
+ template.parentNode?.replaceChild(comment, template);
741
+ template.removeAttribute('d-each');
742
+ // Track clones by key to preserve DOM state on reorder
743
+ const clonesByKey = new Map();
744
+ const disposesByKey = new Map();
745
+ const metadataByKey = new Map();
746
+ const itemSignalsByKey = new Map();
747
+ function createClone(key, value, index, count) {
748
+ const clone = template.cloneNode(true);
749
+ // Create context for this item
750
+ const itemCtx = Object.create(ctx);
751
+ // Create signal for item so bindings can react to updates
752
+ const itemSignal = signal(value);
753
+ // Create signals for spread properties (if value is an object)
754
+ // This allows {propName} bindings to update when value changes
755
+ const spreadProps = new Map();
756
+ if (typeof value === 'object' && value !== null) {
757
+ for (const [propKey, propValue] of Object.entries(value)) {
758
+ const propSignal = signal(propValue);
759
+ spreadProps.set(propKey, propSignal);
760
+ itemCtx[propKey] = propSignal;
761
+ }
762
+ }
763
+ itemSignalsByKey.set(key, { item: itemSignal, spreadProps });
764
+ // Use signals for metadata so they can be updated on reorder
765
+ const metadata = {
766
+ $index: signal(index),
767
+ $count: signal(count),
768
+ $first: signal(index === 0),
769
+ $last: signal(index === count - 1),
770
+ $odd: signal(index % 2 !== 0),
771
+ $even: signal(index % 2 === 0),
772
+ };
773
+ metadataByKey.set(key, metadata);
774
+ // Expose item signal and metadata to context
775
+ itemCtx.item = itemSignal;
776
+ itemCtx.key = key;
777
+ itemCtx.$index = metadata.$index;
778
+ itemCtx.$count = metadata.$count;
779
+ itemCtx.$first = metadata.$first;
780
+ itemCtx.$last = metadata.$last;
781
+ itemCtx.$odd = metadata.$odd;
782
+ itemCtx.$even = metadata.$even;
783
+ // Pass form reference for bindField/bindError to use
784
+ // When clone is detached, el.closest('form[d-form]') returns null
785
+ itemCtx._formElement = formEl;
786
+ itemCtx._formBinding = formBinding;
787
+ // Expose array operations bound to this item's key (not index)
788
+ itemCtx.$remove = () => fieldArray.remove(key);
789
+ itemCtx.$moveUp = () => {
790
+ const currentIndex = fieldArray._getIndex(key);
791
+ if (currentIndex > 0)
792
+ fieldArray.move(currentIndex, currentIndex - 1);
793
+ };
794
+ itemCtx.$moveDown = () => {
795
+ const currentIndex = fieldArray._getIndex(key);
796
+ if (currentIndex < fieldArray.length() - 1)
797
+ fieldArray.move(currentIndex, currentIndex + 1);
798
+ };
799
+ // Mark and bind clone
800
+ clone.setAttribute('data-dalila-internal-bound', '');
801
+ clone.setAttribute('data-array-key', key);
802
+ // Update field names and d-field to include full path
803
+ // Include clone root itself (for primitive arrays like <input d-each="items" d-field="value">)
804
+ const fields = [];
805
+ if (clone.hasAttribute('d-field'))
806
+ fields.push(clone);
807
+ fields.push(...Array.from(clone.querySelectorAll('[d-field]')));
808
+ for (const field of fields) {
809
+ const relativeFieldPath = field.getAttribute('d-field');
810
+ if (relativeFieldPath) {
811
+ const fullPath = `${arrayPath}[${index}].${relativeFieldPath}`;
812
+ field.setAttribute('name', fullPath);
813
+ // Set data-field-path for bindField to use full path
814
+ field.setAttribute('data-field-path', fullPath);
815
+ }
816
+ }
817
+ // Also update d-error elements to use full path (including root)
818
+ const errors = [];
819
+ if (clone.hasAttribute('d-error'))
820
+ errors.push(clone);
821
+ errors.push(...Array.from(clone.querySelectorAll('[d-error]')));
822
+ for (const errorEl of errors) {
823
+ const relativeErrorPath = errorEl.getAttribute('d-error');
824
+ if (relativeErrorPath) {
825
+ const fullPath = `${arrayPath}[${index}].${relativeErrorPath}`;
826
+ errorEl.setAttribute('data-error-path', fullPath);
827
+ }
828
+ }
829
+ // Update nested d-array elements to use full path (for nested field arrays)
830
+ const nestedArrays = clone.querySelectorAll('[d-array]');
831
+ for (const nestedArr of nestedArrays) {
832
+ const relativeArrayPath = nestedArr.getAttribute('d-array');
833
+ if (relativeArrayPath) {
834
+ const fullPath = `${arrayPath}[${index}].${relativeArrayPath}`;
835
+ nestedArr.setAttribute('data-array-path', fullPath);
836
+ }
837
+ }
838
+ // Set type="button" on array control buttons to prevent form submit
839
+ // Buttons like d-on-click="$remove" inside templates aren't processed by
840
+ // bindArrayOperations (they don't exist yet), so set it here during clone creation
841
+ const controlButtons = clone.querySelectorAll('button[d-on-click*="$remove"], button[d-on-click*="$moveUp"], button[d-on-click*="$moveDown"], button[d-on-click*="$swap"]');
842
+ for (const btn of controlButtons) {
843
+ if (btn.getAttribute('type') !== 'button') {
844
+ btn.setAttribute('type', 'button');
845
+ }
846
+ }
847
+ const dispose = bind(clone, itemCtx, { _skipLifecycle: true });
848
+ disposesByKey.set(key, dispose);
849
+ clonesByKey.set(key, clone);
850
+ return clone;
851
+ }
852
+ function updateCloneIndex(clone, key, value, index, count) {
853
+ // Update field names with new index (values stay in DOM)
854
+ // Include clone root itself (for primitive arrays)
855
+ const fields = [];
856
+ if (clone.hasAttribute('d-field'))
857
+ fields.push(clone);
858
+ fields.push(...Array.from(clone.querySelectorAll('[d-field]')));
859
+ for (const field of fields) {
860
+ const relativeFieldPath = field.getAttribute('d-field');
861
+ if (relativeFieldPath) {
862
+ const fullPath = `${arrayPath}[${index}].${relativeFieldPath}`;
863
+ field.setAttribute('name', fullPath);
864
+ field.setAttribute('data-field-path', fullPath);
865
+ }
866
+ }
867
+ // Update d-error elements (including root)
868
+ const errors = [];
869
+ if (clone.hasAttribute('d-error'))
870
+ errors.push(clone);
871
+ errors.push(...Array.from(clone.querySelectorAll('[d-error]')));
872
+ for (const errorEl of errors) {
873
+ const relativeErrorPath = errorEl.getAttribute('d-error');
874
+ if (relativeErrorPath) {
875
+ const fullPath = `${arrayPath}[${index}].${relativeErrorPath}`;
876
+ errorEl.setAttribute('data-error-path', fullPath);
877
+ }
878
+ }
879
+ // Update nested d-array paths
880
+ const nestedArrays = clone.querySelectorAll('[d-array]');
881
+ for (const nestedArr of nestedArrays) {
882
+ const relativeArrayPath = nestedArr.getAttribute('d-array');
883
+ if (relativeArrayPath) {
884
+ const fullPath = `${arrayPath}[${index}].${relativeArrayPath}`;
885
+ nestedArr.setAttribute('data-array-path', fullPath);
886
+ }
887
+ }
888
+ // Update metadata signals with new index values
889
+ const metadata = metadataByKey.get(key);
890
+ if (metadata) {
891
+ metadata.$index.set(index);
892
+ metadata.$count.set(count);
893
+ metadata.$first.set(index === 0);
894
+ metadata.$last.set(index === count - 1);
895
+ metadata.$odd.set(index % 2 !== 0);
896
+ metadata.$even.set(index % 2 === 0);
897
+ }
898
+ // Update item signals when value changes via updateAt()
899
+ const itemSignals = itemSignalsByKey.get(key);
900
+ if (itemSignals) {
901
+ // Update the item signal
902
+ itemSignals.item.set(value);
903
+ // Update spread property signals
904
+ if (typeof value === 'object' && value !== null) {
905
+ const newProps = new Set(Object.keys(value));
906
+ // Update existing props and clear removed ones
907
+ for (const [propKey, propSignal] of itemSignals.spreadProps) {
908
+ if (newProps.has(propKey)) {
909
+ propSignal.set(value[propKey]);
910
+ }
911
+ else {
912
+ // Property was removed - clear to undefined
913
+ propSignal.set(undefined);
914
+ }
915
+ }
916
+ }
917
+ else {
918
+ // Value is not an object (null, primitive, etc) - clear all spread props
919
+ for (const [, propSignal] of itemSignals.spreadProps) {
920
+ propSignal.set(undefined);
921
+ }
922
+ }
923
+ }
924
+ }
925
+ function renderList() {
926
+ const items = fieldArray.fields();
927
+ const newKeys = new Set(items.map((item) => item.key));
928
+ // Remove clones for keys that no longer exist
929
+ for (const [key, clone] of clonesByKey) {
930
+ if (!newKeys.has(key)) {
931
+ clone.remove();
932
+ clonesByKey.delete(key);
933
+ metadataByKey.delete(key);
934
+ itemSignalsByKey.delete(key);
935
+ const dispose = disposesByKey.get(key);
936
+ if (dispose) {
937
+ dispose();
938
+ disposesByKey.delete(key);
939
+ }
940
+ }
941
+ }
942
+ // Build new DOM order, reusing existing clones
943
+ const parent = comment.parentNode;
944
+ if (!parent)
945
+ return;
946
+ // Collect all clones in new order
947
+ const orderedClones = [];
948
+ for (let i = 0; i < items.length; i++) {
949
+ const { key, value } = items[i];
950
+ let clone = clonesByKey.get(key);
951
+ if (clone) {
952
+ // Reuse existing clone, update index-based attributes and item value
953
+ updateCloneIndex(clone, key, value, i, items.length);
954
+ }
955
+ else {
956
+ // Create new clone for new key
957
+ clone = createClone(key, value, i, items.length);
958
+ }
959
+ orderedClones.push(clone);
960
+ }
961
+ // Reorder DOM nodes efficiently
962
+ // Remove all clones from current positions
963
+ for (const clone of orderedClones) {
964
+ if (clone.parentNode) {
965
+ clone.parentNode.removeChild(clone);
966
+ }
967
+ }
968
+ // Insert in correct order before the comment
969
+ for (const clone of orderedClones) {
970
+ parent.insertBefore(clone, comment);
971
+ }
972
+ }
973
+ // Reactive rendering
974
+ effect(() => {
975
+ renderList();
976
+ });
977
+ // Bind array operation buttons
978
+ bindArrayOperations(el, fieldArray, cleanups);
979
+ cleanups.push(() => {
980
+ for (const clone of clonesByKey.values()) {
981
+ clone.remove();
982
+ }
983
+ for (const dispose of disposesByKey.values()) {
984
+ dispose();
985
+ }
986
+ clonesByKey.clear();
987
+ disposesByKey.clear();
988
+ metadataByKey.clear();
989
+ itemSignalsByKey.clear();
990
+ });
991
+ }
992
+ }
993
+ /**
994
+ * Bind array operation buttons: d-append, d-remove, d-insert, d-move-up, d-move-down, d-swap
995
+ */
996
+ function bindArrayOperations(container, fieldArray, cleanups) {
997
+ // d-append: append new item
998
+ const appendButtons = container.querySelectorAll('[d-append]');
999
+ for (const btn of appendButtons) {
1000
+ // Set type="button" to prevent form submit
1001
+ // Inside <form>, buttons default to type="submit"
1002
+ if (btn.getAttribute('type') !== 'button' && btn.tagName === 'BUTTON') {
1003
+ btn.setAttribute('type', 'button');
1004
+ }
1005
+ const handler = (e) => {
1006
+ e.preventDefault(); // Extra safety
1007
+ const defaultValue = btn.getAttribute('d-append');
1008
+ try {
1009
+ const value = defaultValue ? JSON.parse(defaultValue) : {};
1010
+ fieldArray.append(value);
1011
+ }
1012
+ catch {
1013
+ fieldArray.append({});
1014
+ }
1015
+ };
1016
+ btn.addEventListener('click', handler);
1017
+ cleanups.push(() => btn.removeEventListener('click', handler));
1018
+ }
1019
+ // d-remove: remove item (uses context from bindArray)
1020
+ const removeButtons = container.querySelectorAll('[d-remove]');
1021
+ for (const btn of removeButtons) {
1022
+ // This is handled in the item context during bindArray
1023
+ // Just prevent default if it's a button
1024
+ if (btn.getAttribute('type') !== 'button' && btn.tagName === 'BUTTON') {
1025
+ btn.setAttribute('type', 'button');
1026
+ }
1027
+ }
1028
+ // d-move-up, d-move-down: handled in item context
1029
+ const moveButtons = container.querySelectorAll('[d-move-up], [d-move-down]');
1030
+ for (const btn of moveButtons) {
1031
+ if (btn.getAttribute('type') !== 'button' && btn.tagName === 'BUTTON') {
1032
+ btn.setAttribute('type', 'button');
1033
+ }
1034
+ }
1035
+ }
1036
+ // ============================================================================
488
1037
  // Main bind() Function
489
1038
  // ============================================================================
490
1039
  /**
@@ -524,9 +1073,13 @@ export function bind(root, ctx, options = {}) {
524
1073
  const cleanups = [];
525
1074
  // Run all bindings within the template scope
526
1075
  withScope(templateScope, () => {
527
- // 1. d-each — must run first: removes templates before TreeWalker visits them
1076
+ // 1. Form setup — must run very early to register form instances
1077
+ bindForm(root, ctx, cleanups);
1078
+ // 2. d-array — must run before d-each to setup field arrays
1079
+ bindArray(root, ctx, cleanups);
1080
+ // 3. d-each — must run early: removes templates before TreeWalker visits them
528
1081
  bindEach(root, ctx, cleanups);
529
- // 2. Text interpolation
1082
+ // 4. Text interpolation
530
1083
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
531
1084
  const textNodes = [];
532
1085
  // Same boundary logic as qsaIncludingRoot: only visit text nodes that
@@ -553,17 +1106,22 @@ export function bind(root, ctx, options = {}) {
553
1106
  for (const node of textNodes) {
554
1107
  bindTextNode(node, ctx, cleanups);
555
1108
  }
556
- // 3. d-attr bindings
1109
+ // 5. d-attr bindings
557
1110
  bindAttrs(root, ctx, cleanups);
558
- // 4. d-html bindings
1111
+ // 6. d-html bindings
559
1112
  bindHtml(root, ctx, cleanups);
560
- // 5. Event bindings
1113
+ // 7. Form fields — register fields with form instances
1114
+ bindField(root, ctx, cleanups);
1115
+ // 8. Event bindings
561
1116
  bindEvents(root, ctx, events, cleanups);
562
- // 6. d-when directive
1117
+ // 9. d-when directive
563
1118
  bindWhen(root, ctx, cleanups);
564
- // 7. d-match directive
1119
+ // 10. d-match directive
565
1120
  bindMatch(root, ctx, cleanups);
566
- // 8. d-ifmust run last: elements are fully bound before conditional removal
1121
+ // 11. Form error displays BEFORE d-if to bind errors in conditionally rendered sections
1122
+ bindError(root, ctx, cleanups);
1123
+ bindFormError(root, ctx, cleanups);
1124
+ // 12. d-if — must run last: elements are fully bound before conditional removal
567
1125
  bindIf(root, ctx, cleanups);
568
1126
  });
569
1127
  // Bindings complete: remove loading state and mark as ready.
@@ -0,0 +1,2 @@
1
+ import type { Accordion, AccordionOptions } from "./ui-types.js";
2
+ export declare function createAccordion(options?: AccordionOptions): Accordion;