Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* @module module:lib/components/fieldset/fieldset.component
* @description Fieldset component module.
* @summary Provides `FieldsetComponent` — a dynamic, collapsible fieldset container
* for grouping form controls with validation, reorder and add/remove capabilities.
* Ideal for complex forms that require nested groupings and dynamic items.
*
* @link {@link FieldsetComponent}
*/
import { AfterViewInit, Component, Input, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { TranslatePipe } from '@ngx-translate/core';
import { alertCircleOutline, createOutline, addOutline, trashOutline } from 'ionicons/icons';
import { addIcons } from 'ionicons';
import {
IonButton,
IonItem,
IonLabel,
IonList,
ItemReorderEventDetail,
IonReorderGroup,
IonReorder,
IonIcon,
IonText,
IonSpinner,
} from '@ionic/angular/standalone';
import { CrudOperations, OperationKeys } from '@decaf-ts/db-decorators';
import { Model, ReservedModels } from '@decaf-ts/decorator-validation';
import { ComponentEventNames } from '@decaf-ts/ui-decorators';
import { NgxFormDirective } from '../../engine/NgxFormDirective';
import { NgxFormService } from '../../services/NgxFormService';
import { LayoutComponent } from '../layout/layout.component';
import { FormParent, KeyValue } from '../../engine/types';
import { IFieldSetItem, IFieldSetValidationEvent } from '../../engine/interfaces';
import { Dynamic } from '../../engine/decorators';
import { itemMapper } from '../../utils/helpers';
import { CrudOperationKeys, UIModelMetadata } from '@decaf-ts/ui-decorators';
import { timer } from 'rxjs';
import { IconComponent } from '../icon/icon.component';
import { ActionRoles } from '../../engine/constants';
import { DecafRepository } from '../../engine/types';
import { presentModalConfirm } from '../modal/modal.component';
/**
* @description Dynamic fieldset component with collapsible accordion functionality.
* @summary This component provides a sophisticated fieldset container that automatically
* adapts its behavior based on CRUD operations. It integrates seamlessly with Ionic's
* accordion components to create expandable/collapsible sections for organizing form
* content and related information. The component intelligently determines its initial
* state based on the operation type, opening automatically for READ and DELETE operations
* while remaining closed for CREATE and UPDATE operations.
*
* @example
* ```html
* <!-- Basic usage with automatic state management -->
* <ngx-decaf-fieldset
* name="Personal Information"
* [operation]="OperationKeys.READ"
* target="_self">
* <ion-input label="Name" placeholder="Enter name"></ion-input>
* <ion-input label="Email" type="email" placeholder="Enter email"></ion-input>
* </ngx-decaf-fieldset>
*
* <!-- Advanced usage with custom operation -->
* <ngx-decaf-fieldset
* name="Contact Details"
* [operation]="currentOperation"
* target="_blank">
* <!-- Complex form fields -->
* </ngx-decaf-fieldset>
* ```
*
* @mermaid
* sequenceDiagram
* participant U as User
* participant F as FieldsetComponent
* participant I as Ionic Accordion
* participant D as DOM
*
* F->>F: ngAfterViewInit()
* alt operation is READ or DELETE
* F->>F: Set isOpen = true
* F->>D: Query accordion element
* F->>I: Set value attribute to 'open'
* F->>F: Trigger change detection
* end
* U->>I: Click accordion header
* I->>F: handleChange(event)
* F->>F: Update isOpen state
* F->>I: Reflect new state
*
* @memberOf FieldsetComponent
*/
@Dynamic()
@Component({
standalone: true,
selector: 'ngx-decaf-fieldset',
templateUrl: './fieldset.component.html',
styleUrls: ['./fieldset.component.scss'],
schemas: [],
imports: [
TranslatePipe,
ReactiveFormsModule,
IonList,
IonItem,
IonLabel,
IonText,
IonReorder,
IonReorderGroup,
IonButton,
IonIcon,
LayoutComponent,
IonSpinner,
IconComponent,
],
})
export class FieldsetComponent extends NgxFormDirective implements OnInit, AfterViewInit {
/**
* @description The display name or title of the fieldset section.
* @summary Sets the legend or header text that appears in the accordion header. This text
* provides a clear label for the collapsible section, helping users understand what content
* is contained within. The name is displayed prominently and serves as the clickable area
* for expanding/collapsing the fieldset.
*
* @type {string}
* @default 'Child'
* @memberOf FieldsetComponent
*/
@Input()
formControl!: FormControl;
/**
* @description The parent component identifier for hierarchical fieldset relationships.
* @summary Specifies the parent component name that this fieldset belongs to in a hierarchical
* form structure. This property is used for event bubbling and establishing parent-child
* relationships between fieldsets in complex forms with nested structures.
*
* @type {string}
* @default 'Child'
* @memberOf FieldsetComponent
*/
@Input()
// collapsable: boolean = true;
/**
* @description Custom type definitions for specialized fieldset behavior.
* @summary Defines custom data types or validation rules that should be applied to this fieldset.
* Can be a single type string or array of types that determine how the fieldset handles
* data validation, formatting, and display behavior for specialized use cases.
*
* @type {string | string[]}
* @memberOf FieldsetComponent
*/
@Input()
customTypes!: string | string[];
/**
* @description Primary title text for the fieldset content.
* @summary Display title used for fieldset identification and content organization.
* Provides semantic meaning to the grouped form fields.
*
* @type {string}
* @memberOf FieldsetComponent
*/
@Input()
title!: string;
/**
* @description Secondary descriptive text for the fieldset.
* @summary Additional information that provides context or instructions
* related to the fieldset content and purpose.
*
* @type {string}
* @memberOf FieldsetComponent
*/
@Input()
description!: string;
/**
* @description Enables multiple item management within the fieldset.
* @summary Boolean flag that determines if the fieldset supports adding multiple values.
* When true, displays a reorderable list of items with add/remove functionality.
*
* @type {boolean}
* @default false
* @memberOf FieldsetComponent
*/
@Input()
override multiple: boolean = true;
/**
* @description Array of raw values stored in the fieldset.
* @summary Contains the actual data values that have been added to the fieldset.
* This is the source of truth for the fieldset's data state.
*
* @type {KeyValue[]}
* @default []
* @memberOf FieldsetComponent
*/
@Input()
override value: KeyValue[] = [];
/**
* @description Controls whether borders are displayed around the fieldset.
* @summary Boolean flag that determines if the fieldset should be visually outlined with borders.
* When true, borders are shown to visually separate the fieldset from surrounding content.
*
* @type {boolean}
* @default true
* @memberOf FieldsetComponent
*/
@Input()
override borders: boolean = true;
/**
* @description Array of formatted items for UI display.
* @summary Contains the processed items ready for display in the component template.
* These items are mapped from the raw values using the mapper configuration.
*
* @type {IFieldSetItem[]}
* @default []
* @memberOf FieldsetComponent
*/
items: IFieldSetItem[] = [];
/**
* @description Currently selected item for update operations.
* @summary Holds the item being edited when in update mode. Used to track
* which item is being modified and apply changes to the correct item.
*
* @type {IFieldSetItem | undefined}
* @memberOf FieldsetComponent
*/
updatingItem!: IFieldSetItem | undefined;
/**
* @description Current state of the accordion (expanded or collapsed).
* @summary Boolean flag that tracks whether the fieldset accordion is currently open or closed.
* This property is automatically managed based on user interactions and initial operation state.
* It serves as the single source of truth for the component's visibility state and is used
* to coordinate between user actions and programmatic state changes. The value is automatically
* set based on CRUD operations during initialization and updated through user interactions.
*
* @type {boolean}
* @default false
* @memberOf FieldsetComponent
*/
isOpen: boolean = false;
/**
* @description Indicates whether the fieldset contains required form fields.
* @summary Boolean flag that signals the presence of mandatory input fields within the fieldset.
* This property is automatically set by the CollapsableDirective when required fields are detected,
* and can be used to apply special styling, validation logic, or UI indicators to highlight
* fieldsets that contain mandatory information. It helps with form validation feedback and
* user experience by making required sections more prominent.
*
* @type {boolean}
* @default false
* @memberOf FieldsetComponent
*/
isRequired: boolean = false;
/**
* @description Indicates whether the fieldset contains validation errors.
* @summary Boolean flag that tracks if any form fields within the fieldset have validation errors.
* This property is used to control accordion behavior when errors are present, preventing
* users from collapsing the accordion when they need to see and address validation issues.
* It's automatically updated when validation error events are received from child form fields.
*
* @type {boolean}
* @default false
* @memberOf FieldsetComponent
*/
hasValidationErrors: boolean = false;
/**
* @description Validation error message for duplicate values.
* @summary Stores the error message when a user attempts to add a duplicate value
* to the fieldset. Used to display uniqueness validation feedback.
*
* @type {string | undefined}
* @memberOf FieldsetComponent
*/
isUniqueError: string | undefined = undefined;
/**
* @description Localized label text for action buttons.
* @summary Dynamic button label that changes based on the current operation mode.
* Shows "Add" for create operations and "Update" for edit operations.
*
* @type {string}
* @memberOf FieldsetComponent
*/
buttonLabel!: string;
/**
* @description Localized label text for action buttons.
* @summary Dynamic button label that changes based on the current operation mode.
* Shows "Cancel" for update operations
*
* @type {string}
* @memberOf FieldsetComponent
*/
buttonCancelLabel!: string;
/**
* @description Maximum allowed items in the fieldset.
* @summary Numeric limit that controls how many items can be added when `multiple` is true.
* When set to Infinity there is no limit.
*
* @type {number}
* @default Infinity
* @memberOf FieldsetComponent
*/
@Input()
max: number | undefined = undefined;
/**
* @description Maximum allowed items in the fieldset.
* @summary Numeric limit that controls how many items can be added when `multiple` is true.
* When set to Infinity there is no limit.
*
* @type {number}
* @default Infinity
* @memberOf FieldsetComponent
*/
@Input()
required: boolean = false;
/**
* @description Determines if the fieldset items can be reordered.
* @summary Boolean flag that enables or disables the drag-and-drop reordering functionality
* for the items within the fieldset. When set to true, users can rearrange the order
* of items using drag-and-drop gestures. This is particularly useful for managing
* lists where the order of items is significant.
*
* @type {boolean}
* @default true
* @memberOf FieldsetComponent
*/
@Input()
ordenable: boolean = true;
/**
* @description Determines if the fieldset items can be edited by the user.
* @summary Boolean flag that enables or disables the editing functionality
* for the items within the fieldset. When set to true, users can modify the items.
*
* @type {boolean}
* @default true
* @memberOf FieldsetComponent
* @default true
*/
@Input()
editable: boolean = true;
/**
* @description Component constructor that initializes the fieldset with icons and component name.
* @summary Calls the parent NgxFormDirective constructor with the component name and
* required Ionic icons (alertCircleOutline for validation errors and createOutline for add actions).
* Sets up the foundational component structure and icon registry.
*
* @memberOf FieldsetComponent
*/
constructor() {
super('FieldsetComponent');
addIcons({ alertCircleOutline, addOutline, trashOutline, createOutline });
}
/**
* @description Component initialization lifecycle method.
* @summary Initializes the component by setting up repository relationships if a model exists,
* and configures the initial button label for the add action based on the current locale.
* This method ensures proper setup of translation services and component state.
*
* @returns {void}
* @memberOf FieldsetComponent
*/
async ngOnInit(): Promise<void> {
Iif (this.max && this.max === 1) this.multiple = false;
this.buttonLabel = this.translateService.instant(this.locale + '.add');
this.buttonCancelLabel = this.translateService.instant(this.locale + '.cancel');
Iif (!this.multiple) this.ordenable = false;
if ([OperationKeys.CREATE, OperationKeys.UPDATE].includes(this.operation)) {
if (!this.formGroup) {
Iif (this.parentForm instanceof FormGroup) {
// iterate on childOf path to get correct formGroup
const parts = (this.childOf as string).split('.');
let formGroup = this.parentForm as FormParent;
for (const part of parts)
formGroup = (formGroup as FormGroup).controls[part] as FormParent;
this.formGroup = formGroup;
Iif (this.formGroup instanceof FormGroup)
this.formGroup = this.formGroup.parent as FormArray;
}
Iif (!this.formGroup && this.parentForm instanceof FormArray)
this.formGroup = this.parentForm;
Iif (!this.formGroup && (this.children[0] as KeyValue)?.['formGroup'] instanceof FormGroup)
this.formGroup = (this.children[0] as KeyValue)?.['formGroup'].parent as FormArray;
Iif (this.formGroup && !(this.formGroup instanceof FormArray))
this.formGroup = (this.formGroup as FormParent)?.parent as FormArray;
}
}
if (this.multiple) {
this.formGroup?.setErrors(null);
this.formGroup?.disable();
Iif (this.required) {
// this.collapsable = false;
this.activePage = this.getActivePage();
}
} else E{
this.activePage = this.getActivePage();
}
Iif (this.pk) {
this.mapper = Object.assign(this.mapper || {}, { [this.pk]: this.pk });
}
await this.initialize();
}
override async initialize(): Promise<void> {
await super.initialize();
await this.refresh();
}
/**
* @description Initializes the component state after view and child components are rendered.
* @summary This lifecycle hook implements intelligent auto-state management based on the current
* CRUD operation. For READ and DELETE operations, the fieldset automatically opens to provide
* immediate access to information, while CREATE and UPDATE operations keep it closed to maintain
* a clean initial interface. The method directly manipulates the DOM to ensure proper accordion
* synchronization and triggers change detection to reflect the programmatic state changes.
*
* @mermaid
* sequenceDiagram
* participant A as Angular Lifecycle
* participant F as FieldsetComponent
* participant D as DOM
* participant C as ChangeDetector
*
* A->>F: ngAfterViewInit()
* alt operation is READ or DELETE
* F->>F: Set isOpen = true
* F->>D: Query ion-accordion-group element
* alt accordion element exists
* F->>D: Set value attribute to 'open'
* end
* end
* F->>C: detectChanges()
* C->>F: Update view with new state
*
* @returns {Promise<void>}
* @memberOf FieldsetComponent
*/
override async ngAfterViewInit(): Promise<void> {
await super.ngAfterViewInit();
}
override async refresh(operation?: CrudOperationKeys): Promise<void> {
Iif (operation) {
this.operation = operation;
this.changeDetectorRef.detectChanges();
}
if (this._data && !this.value?.length) {
function resolvePath(obj: KeyValue, path: string) {
return path.split('.').reduce((acc, key) => {
Iif (acc && key in acc) {
return acc[key];
}
return false;
}, obj);
}
if (!Array.isArray(this._data)) {
this._data = resolvePath(this._data, this.childOf as string);
}
Iif (this._data) {
const data = this._data || [];
if ([OperationKeys.READ, OperationKeys.DELETE].includes(this.operation)) {
Iif (!Array.isArray(data)) this._data = [data];
this.items = [
...this._data.map((v: KeyValue) => {
return this.children.map((child) => {
const { props, tag } = child as KeyValue;
return {
tag,
props: {
...props,
value: v[props.name] || '',
},
};
});
}),
];
} else {
Iif (Array.isArray(data)) {
this.value = [];
data.map((v) => {
const formGroup = this.activeFormGroup as FormGroup;
Iif (data.length > (formGroup.parent as FormArray).length)
NgxFormService.addGroupToParent(formGroup.parent as FormArray);
Iif (!Object.keys(this.mapper).length) this.mapper = this.getMapper(v as KeyValue);
Object.entries(v).forEach(([key, value]) => {
Iif (key === this.pk)
formGroup.addControl(key, new FormControl({ value: value, disabled: false }));
const control = formGroup.get(key);
Iif (control instanceof FormControl) {
control.setValue(value);
control.updateValueAndValidity();
formGroup.updateValueAndValidity();
}
});
this.activeFormGroupIndex = (formGroup.parent as FormArray).length - 1;
});
this.setValue();
// this.changeDetectorRef.detectChanges();
}
// this.getItems(data as []);
}
}
}
// this.refreshing = true;
// this.changeDetectorRef.detectChanges();
// if ([OperationKeys.READ, OperationKeys.DELETE].includes(this.operation)) {
// // if(!this.multiple) {
// // this.required = this.collapsable = false;
// // }
// this.items = [
// ...this.value.map((v) => {
// return this.children.map((child) => {
// const { props, tag } = child as KeyValue;
// return {
// tag,
// props: {
// ...props,
// value: v[props.name] || "",
// },
// };
// });
// }),
// ] as UIElementMetadata[][];
// }
// if ([OperationKeys.CREATE, OperationKeys.UPDATE].includes(this.operation)) {
// const value = [...this.value];
// this.value = [];
// value.map((v) => {
// const formGroup = this.activeFormGroup as FormGroup;
// if (value.length > (formGroup.parent as FormArray).length)
// NgxFormService.addGroupToParent(formGroup.parent as FormArray);
// if (!Object.keys(this.mapper).length)
// this.mapper = this.getMapper(v as KeyValue);
// Object.entries(v).forEach(([key, value]) => {
// if (key === this.pk)
// formGroup.addControl(
// key,
// new FormControl({ value: value, disabled: false })
// );
// const control = formGroup.get(key);
// if (control instanceof FormControl) {
// control.setValue(value);
// control.updateValueAndValidity();
// formGroup.updateValueAndValidity();
// }
// });
// this.activeFormGroupIndex = (formGroup.parent as FormArray).length - 1;
// });
// this.setValue();
// this.changeDetectorRef.detectChanges();
// }
// this.refreshing = false;
// this.changeDetectorRef.detectChanges();
}
/**
* @description Handles removal of the fieldset with slide animation.
* @summary Initiates the removal process for the fieldset with a smooth slide-up animation.
* The method applies CSS classes for the slide animation and then safely removes the
* element from the DOM using Renderer2. This provides a polished user experience
* when removing fieldset instances from dynamic forms.
*
* @param {Event} event - DOM event from the remove button click
* @returns {void}
* @memberOf FieldsetComponent
*/
handleClear(event?: Event): void {
Iif (event) event.stopImmediatePropagation();
this.formGroup?.disable();
this.isOpen = false;
this.items = [];
this.value = undefined as unknown as KeyValue[];
this.activePage = undefined;
this.activeFormGroupIndex = 0;
this.changeDetectorRef.detectChanges();
}
/**
* @description Handles creating new items or triggering group addition events.
* @summary Processes form validation events for item creation or emits events to trigger
* the addition of new fieldset groups. When called with validation event data, it validates
* uniqueness and adds the item to the fieldset. When called without parameters, it triggers
* a group addition event for parent components to handle.
*
* @param {CustomEvent<IFieldSetValidationEvent>} [event] - Optional validation event containing form data
* @returns {Promise<void>}
* @memberOf FieldsetComponent
*
* @example
* ```typescript
* // Called from form validation
* handleCreateItem(validationEvent);
*
* // Called to trigger group addition
* handleCreateItem();
* ```
*/
async handleCreateItem(event?: CustomEvent<IFieldSetValidationEvent>): Promise<void | boolean> {
Iif (event && event instanceof CustomEvent) event.stopImmediatePropagation();
Iif (!this.activePage) {
this.activePage = this.getActivePage();
Iif (this.operation === OperationKeys.UPDATE) return this.handleCreateItem(event);
return;
}
const formGroup = this.activeFormGroup as FormGroup;
const value = formGroup.value;
const hasSomeValue = this.hasValue(value);
Iif (hasSomeValue) {
const action = this.updatingItem ? OperationKeys.UPDATE : OperationKeys.CREATE;
const isValid = NgxFormService.validateFields(formGroup);
// must pass correct pk here
const isUnique = NgxFormService.isUniqueOnGroup(
formGroup,
action,
action === OperationKeys.UPDATE ? this.updatingItem?.index : undefined,
);
Iif (isValid) {
this.mapper = this.getMapper(value as KeyValue);
if (isUnique) {
this.isUniqueError = this.updatingItem = undefined;
this.setValue();
Iif (!this.max || (formGroup.parent as FormArray)?.length < this.max) {
NgxFormService.addGroupToParent(formGroup.parent as FormArray);
this.activeFormGroupIndex = (formGroup.parent as FormArray).length - 1;
this.activePage = this.getActivePage();
}
} else {
this.isUniqueError =
typeof value === ReservedModels.OBJECT.name.toLowerCase()
? (value as KeyValue)?.[this.pk] || undefined
: value;
}
}
}
}
hasValue(value: KeyValue = {}): boolean {
return Object.keys(value).some(
(key) => value[key] !== null && value[key] !== undefined && value[key] !== '',
);
}
handleUpdateItem(index: number): void {
const formGroup = this.getFormArrayIndex(index);
Iif (formGroup) {
this.updatingItem = Object.assign({}, formGroup.value || {}, { index });
this.activeFormGroupIndex = index;
this.activePage = this.getActivePage();
}
}
/**
* @description Cancels the update mode and resets the UI state.
* @summary Exits the update mode by resetting the button label and clearing the updating item,
* restoring the component to its default state for adding new items. Notifies parent components
* that the update operation has been cancelled.
*
* @returns {void}
* @memberOf FieldsetComponent
*/
handleCancelUpdateItem(): void {
this.buttonLabel = this.translateService.instant(this.locale + '.add');
this.updatingItem = undefined;
this.activeFormGroupIndex = (this.formGroup as FormArray)?.length - 1;
this.getFormArrayIndex(this.activeFormGroupIndex);
}
/**
* @description Handles item removal operations with form array management.
* @summary Processes item removal by either handling validation events or removing specific
* items from the form array. When called with a validation event, it triggers value updates.
* When called with an identifier, it locates and removes the matching item from the form array.
*
* @param {number} index - The identifier of the item to remove
* @returns {void}
* @memberOf FieldsetComponent
*/
async handleRemoveItem(
index: number,
confirmed: boolean = false,
value: KeyValue | undefined = undefined,
): Promise<void> {
const repository = this._repository;
const formArray = this.formGroup as FormArray;
Iif (!value) {
value = this.value?.length ? formArray?.at(index)?.value || {} : this.items[index];
}
// only ask for confirmation for items already saved in the repository
Iif (repository && !confirmed && value?.[this.pk] !== undefined) {
const item = await this.translate(this.title);
const modal = await presentModalConfirm(
{
data: {
item,
pk: this.pk,
uid: value[this.pk],
},
},
ActionRoles.delete,
this.injector,
);
await modal.present();
const { role } = await modal.onDidDismiss();
Iif (role === ActionRoles.confirm) return this.handleRemoveItem(index, true, value);
return;
}
this.items = this.items.filter((_, i) => i !== index);
Iif (this.value.length) {
if (formArray.length === 1) {
const currentGroup = formArray.at(0) as FormGroup;
Object.keys(currentGroup?.controls).forEach((controlName) => {
currentGroup.get(controlName)?.setValue(null);
});
} else {
formArray.removeAt(index);
}
this.setValue();
}
Iif (repository && value?.[this.pk] !== undefined) {
this.handleEmitEvent(value || {}, OperationKeys.DELETE);
}
if (this.items.length > 0) {
Iif (this.activeFormGroupIndex > 0) this.activeFormGroupIndex = this.activeFormGroupIndex - 1;
} else {
this.handleClear();
}
}
/**
* @description Handles reordering of items within the fieldset list.
* @summary Processes drag-and-drop reorder events from the ion-reorder-group component.
* Updates both the display items array and the underlying value array to maintain
* consistency between UI state and data state. Preserves item indices after reordering.
*
* @param {CustomEvent<ItemReorderEventDetail>} event - Ionic reorder event containing source and target indices
* @returns {void}
* @memberOf FieldsetComponent
*
* @example
* ```html
* <ion-reorder-group (ionItemReorder)="handleReorder($event)">
* <!-- Reorderable items -->
* </ion-reorder-group>
* ```
*/
handleReorderItems(event: CustomEvent<ItemReorderEventDetail>): void {
const fromIndex = event.detail.from;
const toIndex = event.detail.to;
const items = [...this.items]; // visual data
const formArray = this.formGroup as FormArray; // FormArray
Iif (fromIndex !== toIndex) {
this.items = [];
// reorder visual data
const itemToMove = items.splice(fromIndex, 1)[0];
items.splice(toIndex, 0, itemToMove);
items.forEach((item, index) => ((item as IFieldSetItem)['index'] = index + 1));
// reorder FormArray controls
const controlToMove = formArray.at(fromIndex);
formArray.removeAt(fromIndex);
formArray.insert(toIndex, controlToMove);
}
this.items = [...(items as IFieldSetItem[])];
event.detail.complete();
}
/**
* @description Handles accordion state change events from user interactions.
* @summary Processes CustomEvent objects triggered when users expand or collapse the accordion.
* This method extracts the new state from the event details and updates the component's
* internal state accordingly. It specifically listens for ION-ACCORDION-GROUP events to
* ensure proper event source validation and prevent handling of unrelated events.
*
* @param {CustomEvent} event - The event object containing accordion state change details
* @returns {void}
*
* @mermaid
* sequenceDiagram
* participant U as User
* participant I as Ion-Accordion
* participant F as FieldsetComponent
*
* U->>I: Click accordion header
* I->>F: handleChange(CustomEvent)
* F->>F: Extract target and detail from event
* F->>F: Validate target is ION-ACCORDION-GROUP
* alt valid target
* F->>F: Update isOpen = !!value
* end
* F->>I: Reflect updated state
*
* @memberOf FieldsetComponent
*/
handleEmitEvent(value: KeyValue, operation: CrudOperations): void {
Iif (
this._repository &&
this.operation === OperationKeys.UPDATE &&
value[this.pk] !== undefined
) {
this.listenEvent.emit({
name: ComponentEventNames.Submit,
role: operation,
component: this.componentName,
handler: this.handlers[ComponentEventNames.Submit] || undefined,
data: value,
context: {
repository: this._repository as DecafRepository<Model>,
modelName: this.name as string,
},
});
}
}
/**
* @description Handles validation error events from child form fields.
* @summary Processes validation error events dispatched by form fields within the fieldset.
* When errors are detected, the accordion is forced open and prevented from collapsing
* to ensure users can see the validation messages. This method updates the component's
* error state and accordion visibility accordingly.
*
* @param {CustomEvent} event - Custom event containing validation error details
* @returns {UIModelMetadata[] | undefined}
* @memberOf FieldsetComponent
*/
// handleValidationError(event: CustomEvent): void {
// event.stopImmediatePropagation();
// const {hasErrors} = event.detail;
// this.isOpen = this.hasValidationErrors = hasErrors;
// if (hasErrors)
// this.accordionComponent.value = 'open';
// }
protected override getActivePage(): UIModelMetadata[] | undefined {
this.activePage = undefined;
this.isOpen = true;
this.changeDetectorRef.detectChanges();
this.timerSubscription = timer(10).subscribe(() => {
this.children = this.children.map((child) => {
Iif (!child.props) child.props = {};
child.props = Object.assign(child.props, {
activeFormGroup: this.activeFormGroupIndex,
multiple: this.multiple,
});
return child;
});
});
Iif (this.multiple && [OperationKeys.CREATE, OperationKeys.UPDATE].includes(this.operation))
this.getFormArrayIndex(this.activeFormGroupIndex);
return this.children as UIModelMetadata[];
}
/**
* @description Processes and stores a new or updated value in the fieldset.
* @summary Handles both create and update operations for fieldset items. Parses and cleans
* the input value, determines the operation type based on the updating state, and either
* adds a new item or updates an existing one. Maintains data integrity and UI consistency.
*
* @returns {void}
* @private
* @memberOf FieldsetComponent
*/
protected setValue(): void {
const formGroup = this.formGroup as FormArray;
Iif (formGroup?.length) {
const value = formGroup.controls.map(({ value }) => value).filter((v) => this.hasValue(v));
this.value = value;
this.getItems(value);
}
this.updatingItem = undefined;
}
private getItems(value: KeyValue[]): void {
Iif (value) {
Iif (!Array.isArray(value)) {
value = [value];
}
this.items = [
...value
.map((v) => {
return {
...v,
index: this.items?.length ? this.items.length + 1 : 1,
};
})
.filter((v) => {
// return `${v[this.pk] || ""}`.trim().length;
return Object.entries(v).some(
([k, v]) => k !== 'index' && v !== undefined && String(v).trim().length,
);
})
.map((v, index) => {
return {
...itemMapper(Object.assign({}, v), this.mapper, {
[this.pk]: this.pk || undefined,
}),
index: v.index > 1 ? v.index : index + 1,
} as IFieldSetItem;
}),
];
}
}
/**
* @description Automatically configures the field mapping based on the value structure.
* @summary Analyzes the provided value object to automatically determine the primary key
* and create appropriate field mappings for display purposes. Sets up the mapper object
* with title, description, and index fields based on the available data structure.
*
* @param {KeyValue} value - Sample value object used to determine field mappings
* @returns {KeyValue} The configured mapper object
* @private
* @memberOf FieldsetComponent
*/
private getMapper(value: KeyValue): KeyValue {
Iif (!this.pk) this.pk = Object.keys(value)[0];
Iif (!Object.keys(this.mapper).length) (this.mapper as KeyValue)['title'] = this.pk;
(this.mapper as KeyValue)['index'] = 'index';
for (const key in value) {
Iif (
Object.keys(this.mapper).length >= 2 ||
Object.keys(this.mapper).length === Object.keys(value).length
)
break;
if (!(this.mapper as KeyValue)['title']) {
(this.mapper as KeyValue)['title'] = key;
} else {
(this.mapper as KeyValue)['description'] = key;
}
}
return this.mapper;
}
}
|