@ng-forge/dynamic-forms-ionic 0.5.0 → 0.5.2

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.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, input, computed, ChangeDetectionStrategy, Component, ElementRef, signal, InjectionToken, linkedSignal, model, isSignal } from '@angular/core';
3
3
  import { IonButton, IonCheckbox, IonNote, IonInput, IonModal, IonDatetime, IonButtons, IonHeader, IonContent, IonToolbar, IonTitle, IonItem, IonRadioGroup, IonRadio, IonSelect, IonSelectOption, IonRange, IonTextarea, IonToggle } from '@ionic/angular/standalone';
4
- import { EventBus, ARRAY_CONTEXT, resolveTokens, DynamicTextPipe, DEFAULT_PROPS, buildBaseInputs, RootFormRegistryService, FORM_OPTIONS, resolveSubmitButtonDisabled, FIELD_SIGNAL_CONTEXT, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, DynamicFormLogger, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';
4
+ import { EventBus, ARRAY_CONTEXT, resolveTokens, DynamicTextPipe, DEFAULT_PROPS, buildBaseInputs } from '@ng-forge/dynamic-forms';
5
5
  import { AsyncPipe } from '@angular/common';
6
6
  import { FormField } from '@angular/forms/signals';
7
- import { createResolvedErrorsSignal, shouldShowErrors, setupMetaTracking, isEqual, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, datepickerFieldMapper } from '@ng-forge/dynamic-forms/integration';
7
+ import { createResolvedErrorsSignal, shouldShowErrors, setupMetaTracking, isEqual, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, submitButtonFieldMapper, nextButtonFieldMapper, previousButtonFieldMapper, addArrayItemButtonMapper, prependArrayItemButtonMapper, insertArrayItemButtonMapper, removeArrayItemButtonMapper, popArrayItemButtonMapper, shiftArrayItemButtonMapper, datepickerFieldMapper } from '@ng-forge/dynamic-forms/integration';
8
8
  import { format } from 'date-fns';
9
9
  import { explicitEffect } from 'ngxtension/explicit-effect';
10
10
 
@@ -948,7 +948,7 @@ class IonicSelectFieldComponent {
948
948
  validationMessages = input(...(ngDevMode ? [undefined, { debugName: "validationMessages" }] : []));
949
949
  defaultValidationMessages = input(...(ngDevMode ? [undefined, { debugName: "defaultValidationMessages" }] : []));
950
950
  constructor() {
951
- setupMetaTracking(this.elementRef, this.meta);
951
+ setupMetaTracking(this.elementRef, this.meta, { selector: 'ion-select' });
952
952
  }
953
953
  resolvedErrors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
954
954
  showErrors = shouldShowErrors(this.field);
@@ -1533,7 +1533,11 @@ const IonicField = {
1533
1533
  Next: 'next',
1534
1534
  Previous: 'previous',
1535
1535
  AddArrayItem: 'addArrayItem',
1536
+ PrependArrayItem: 'prependArrayItem',
1537
+ InsertArrayItem: 'insertArrayItem',
1536
1538
  RemoveArrayItem: 'removeArrayItem',
1539
+ PopArrayItem: 'popArrayItem',
1540
+ ShiftArrayItem: 'shiftArrayItem',
1537
1541
  Textarea: 'textarea',
1538
1542
  Radio: 'radio',
1539
1543
  MultiCheckbox: 'multi-checkbox',
@@ -1547,11 +1551,15 @@ const IonicField = {
1547
1551
  * For specific button types (submit, next, prev, add/remove array items),
1548
1552
  * use the dedicated field types and their specific mappers.
1549
1553
  *
1554
+ * Supports template property for array events (AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent)
1555
+ * which enables the $template token in eventArgs.
1556
+ *
1550
1557
  * @param fieldDef The button field definition
1551
1558
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1552
1559
  */
1553
1560
  function buttonFieldMapper(fieldDef) {
1554
1561
  const defaultProps = inject(DEFAULT_PROPS);
1562
+ const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1555
1563
  return computed(() => {
1556
1564
  const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1557
1565
  const inputs = {
@@ -1571,226 +1579,33 @@ function buttonFieldMapper(fieldDef) {
1571
1579
  if ('eventArgs' in fieldDef && fieldDef.eventArgs !== undefined) {
1572
1580
  inputs['eventArgs'] = fieldDef.eventArgs;
1573
1581
  }
1574
- return inputs;
1575
- });
1576
- }
1577
-
1578
- /**
1579
- * Mapper for submit button - configures native form submission via type="submit"
1580
- *
1581
- * Unlike other buttons, submit buttons don't dispatch events directly.
1582
- * Instead, they trigger native form submission which the form component
1583
- * intercepts and dispatches to the EventBus.
1584
- *
1585
- * Disabled state is resolved using the button-logic-resolver which considers:
1586
- * 1. Explicit `disabled: true` on the field definition
1587
- * 2. Field-level `logic` array (if present, overrides form-level defaults)
1588
- * 3. Form-level `options.submitButton` defaults (disableWhenInvalid, disableWhileSubmitting)
1589
- *
1590
- * @param fieldDef The submit button field definition
1591
- * @returns Signal containing Record of input names to values for ngComponentOutlet
1592
- */
1593
- function submitButtonFieldMapper(fieldDef) {
1594
- const rootFormRegistry = inject(RootFormRegistryService);
1595
- const defaultProps = inject(DEFAULT_PROPS);
1596
- const formOptions = inject(FORM_OPTIONS);
1597
- const fieldWithLogic = fieldDef;
1598
- return computed(() => {
1599
- const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1600
- // Use rootFormRegistry instead of fieldSignalContext.form because when the submit button
1601
- // is inside a group/array, fieldSignalContext.form points to the nested form tree,
1602
- // not the root form. We need root form validity for submit button disabled state (#157).
1603
- const disabledSignal = resolveSubmitButtonDisabled({
1604
- form: rootFormRegistry.getRootForm(),
1605
- formOptions: formOptions(),
1606
- fieldLogic: fieldWithLogic.logic,
1607
- explicitlyDisabled: fieldDef.disabled,
1608
- });
1609
- const inputs = {
1610
- ...baseInputs,
1611
- // No event - native form submit handles it via form's onNativeSubmit
1612
- // Set type="submit" to trigger native form submission
1613
- props: { ...fieldDef.props, type: 'submit' },
1614
- // Evaluate the signal inside the computed - component receives plain boolean
1615
- disabled: disabledSignal(),
1616
- };
1617
- if (fieldDef.hidden !== undefined) {
1618
- inputs['hidden'] = fieldDef.hidden;
1619
- }
1620
- return inputs;
1621
- });
1622
- }
1623
- /**
1624
- * Mapper for next page button - preconfigures NextPageEvent
1625
- *
1626
- * Disabled state is resolved using the button-logic-resolver which considers:
1627
- * 1. Explicit `disabled: true` on the field definition
1628
- * 2. Field-level `logic` array (if present, overrides form-level defaults)
1629
- * 3. Form-level `options.nextButton` defaults (disableWhenPageInvalid, disableWhileSubmitting)
1630
- *
1631
- * @param fieldDef The next button field definition
1632
- * @returns Signal containing Record of input names to values for ngComponentOutlet
1633
- */
1634
- function nextButtonFieldMapper(fieldDef) {
1635
- const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1636
- const defaultProps = inject(DEFAULT_PROPS);
1637
- const formOptions = inject(FORM_OPTIONS);
1638
- const fieldWithLogic = fieldDef;
1639
- return computed(() => {
1640
- const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1641
- const disabledSignal = resolveNextButtonDisabled({
1642
- form: fieldSignalContext.form,
1643
- formOptions: formOptions(),
1644
- fieldLogic: fieldWithLogic.logic,
1645
- explicitlyDisabled: fieldDef.disabled,
1646
- currentPageValid: fieldSignalContext.currentPageValid,
1647
- });
1648
- const inputs = {
1649
- ...baseInputs,
1650
- event: NextPageEvent,
1651
- // Evaluate the signal inside the computed - component receives plain boolean
1652
- disabled: disabledSignal(),
1653
- };
1654
- if (fieldDef.hidden !== undefined) {
1655
- inputs['hidden'] = fieldDef.hidden;
1656
- }
1657
- return inputs;
1658
- });
1659
- }
1660
- /**
1661
- * Mapper for previous page button - preconfigures PreviousPageEvent
1662
- * Note: Does not auto-disable based on validation. Users can explicitly disable if needed.
1663
- *
1664
- * @param fieldDef The previous button field definition
1665
- * @returns Signal containing Record of input names to values for ngComponentOutlet
1666
- */
1667
- function previousButtonFieldMapper(fieldDef) {
1668
- const defaultProps = inject(DEFAULT_PROPS);
1669
- return computed(() => {
1670
- const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1671
- const inputs = {
1672
- ...baseInputs,
1673
- event: PreviousPageEvent,
1674
- };
1675
- // Add disabled binding only if explicitly set by user
1676
- if (fieldDef.disabled !== undefined) {
1677
- inputs['disabled'] = fieldDef.disabled;
1678
- }
1679
- if (fieldDef.hidden !== undefined) {
1680
- inputs['hidden'] = fieldDef.hidden;
1681
- }
1682
- return inputs;
1683
- });
1684
- }
1685
- /**
1686
- * Mapper for add array item button - preconfigures AddArrayItemEvent with array context.
1687
- *
1688
- * Supports two modes:
1689
- * 1. Inside array template: Uses ARRAY_CONTEXT to determine target array
1690
- * 2. Outside array: Uses `arrayKey` property from field definition
1691
- *
1692
- * @param fieldDef The add array item button field definition
1693
- * @returns Signal containing Record of input names to values for ngComponentOutlet
1694
- */
1695
- function addArrayItemButtonFieldMapper(fieldDef) {
1696
- const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1697
- const logger = inject(DynamicFormLogger);
1698
- const defaultProps = inject(DEFAULT_PROPS);
1699
- // Determine the target array key
1700
- // Priority: explicit arrayKey from fieldDef > arrayKey from context
1701
- const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
1702
- if (!targetArrayKey) {
1703
- logger.warn(`addArrayItem button "${fieldDef.key}" has no array context. ` +
1704
- 'Either place it inside an array field, or provide an explicit arrayKey property.');
1705
- }
1706
- // Set default eventArgs for AddArrayItemEvent (arrayKey)
1707
- // User can override by providing eventArgs in field definition
1708
- const defaultEventArgs = ['$arrayKey'];
1709
- const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
1710
- return computed(() => {
1711
- const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1712
- // Read signal value if index is a signal (supports differential updates)
1713
- const getIndex = () => {
1714
- if (!arrayContext)
1715
- return -1;
1716
- return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;
1717
- };
1718
- const inputs = {
1719
- ...baseInputs,
1720
- event: AddArrayItemEvent,
1721
- eventArgs,
1722
- eventContext: {
1582
+ // Add eventContext for token resolution (supports $template, $arrayKey, $index, etc.)
1583
+ const template = 'template' in fieldDef ? fieldDef.template : undefined;
1584
+ if (template || arrayContext) {
1585
+ // Read signal value if index is a signal (supports differential updates)
1586
+ const getIndex = () => {
1587
+ if (!arrayContext)
1588
+ return -1;
1589
+ return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;
1590
+ };
1591
+ inputs['eventContext'] = {
1723
1592
  key: fieldDef.key,
1724
1593
  index: getIndex(),
1725
- arrayKey: targetArrayKey ?? '',
1594
+ arrayKey: arrayContext?.arrayKey ?? '',
1726
1595
  formValue: arrayContext?.formValue ?? {},
1727
- },
1728
- };
1729
- // Add disabled binding only if explicitly set by user
1730
- if (fieldDef.disabled !== undefined) {
1731
- inputs['disabled'] = fieldDef.disabled;
1732
- }
1733
- if (fieldDef.hidden !== undefined) {
1734
- inputs['hidden'] = fieldDef.hidden;
1596
+ template,
1597
+ };
1735
1598
  }
1736
1599
  return inputs;
1737
1600
  });
1738
1601
  }
1602
+
1739
1603
  /**
1740
- * Mapper for remove array item button - preconfigures RemoveArrayItemEvent with array context.
1604
+ * Navigation button mappers for Ionic - re-exported from integration package.
1741
1605
  *
1742
- * Supports two modes:
1743
- * 1. Inside array template: Uses ARRAY_CONTEXT to determine target array and removes item at current index
1744
- * 2. Outside array: Uses `arrayKey` property from field definition, removes last item by default
1745
- *
1746
- * @param fieldDef The remove array item button field definition
1747
- * @returns Signal containing Record of input names to values for ngComponentOutlet
1606
+ * These mappers handle submit, next, and previous buttons with proper
1607
+ * disabled state resolution based on form validity and options.
1748
1608
  */
1749
- function removeArrayItemButtonFieldMapper(fieldDef) {
1750
- const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1751
- const logger = inject(DynamicFormLogger);
1752
- const defaultProps = inject(DEFAULT_PROPS);
1753
- // Determine the target array key
1754
- // Priority: explicit arrayKey from fieldDef > arrayKey from context
1755
- const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
1756
- if (!targetArrayKey) {
1757
- logger.warn(`removeArrayItem button "${fieldDef.key}" has no array context. ` +
1758
- 'Either place it inside an array field, or provide an explicit arrayKey property.');
1759
- }
1760
- // Set default eventArgs for RemoveArrayItemEvent (arrayKey, index if inside array)
1761
- // When outside array, only pass arrayKey (removes last by default)
1762
- // User can override by providing eventArgs in field definition
1763
- const defaultEventArgs = arrayContext ? ['$arrayKey', '$index'] : ['$arrayKey'];
1764
- const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
1765
- return computed(() => {
1766
- const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1767
- // Read signal value if index is a signal (supports differential updates)
1768
- const getIndex = () => {
1769
- if (!arrayContext)
1770
- return -1; // -1 means remove last (no specific index)
1771
- return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;
1772
- };
1773
- const inputs = {
1774
- ...baseInputs,
1775
- event: RemoveArrayItemEvent,
1776
- eventArgs,
1777
- eventContext: {
1778
- key: fieldDef.key,
1779
- index: getIndex(),
1780
- arrayKey: targetArrayKey ?? '',
1781
- formValue: arrayContext?.formValue ?? {},
1782
- },
1783
- };
1784
- // Add disabled binding only if explicitly set by user
1785
- if (fieldDef.disabled !== undefined) {
1786
- inputs['disabled'] = fieldDef.disabled;
1787
- }
1788
- if (fieldDef.hidden !== undefined) {
1789
- inputs['hidden'] = fieldDef.hidden;
1790
- }
1791
- return inputs;
1792
- });
1793
- }
1794
1609
 
1795
1610
  /**
1796
1611
  * Ionic field type definitions
@@ -1840,13 +1655,37 @@ const IONIC_FIELD_TYPES = [
1840
1655
  {
1841
1656
  name: IonicField.AddArrayItem,
1842
1657
  loadComponent: () => Promise.resolve().then(function () { return ionicButton_component; }),
1843
- mapper: addArrayItemButtonFieldMapper,
1658
+ mapper: addArrayItemButtonMapper,
1659
+ valueHandling: 'exclude',
1660
+ },
1661
+ {
1662
+ name: IonicField.PrependArrayItem,
1663
+ loadComponent: () => Promise.resolve().then(function () { return ionicButton_component; }),
1664
+ mapper: prependArrayItemButtonMapper,
1665
+ valueHandling: 'exclude',
1666
+ },
1667
+ {
1668
+ name: IonicField.InsertArrayItem,
1669
+ loadComponent: () => Promise.resolve().then(function () { return ionicButton_component; }),
1670
+ mapper: insertArrayItemButtonMapper,
1844
1671
  valueHandling: 'exclude',
1845
1672
  },
1846
1673
  {
1847
1674
  name: IonicField.RemoveArrayItem,
1848
1675
  loadComponent: () => Promise.resolve().then(function () { return ionicButton_component; }),
1849
- mapper: removeArrayItemButtonFieldMapper,
1676
+ mapper: removeArrayItemButtonMapper,
1677
+ valueHandling: 'exclude',
1678
+ },
1679
+ {
1680
+ name: IonicField.PopArrayItem,
1681
+ loadComponent: () => Promise.resolve().then(function () { return ionicButton_component; }),
1682
+ mapper: popArrayItemButtonMapper,
1683
+ valueHandling: 'exclude',
1684
+ },
1685
+ {
1686
+ name: IonicField.ShiftArrayItem,
1687
+ loadComponent: () => Promise.resolve().then(function () { return ionicButton_component; }),
1688
+ mapper: shiftArrayItemButtonMapper,
1850
1689
  valueHandling: 'exclude',
1851
1690
  },
1852
1691
  {