UNPKG

@avoraui/av-dual-list-box

Version:

A customizable Angular dual list box component for managing item selections.

273 lines (268 loc) 21.5 kB
import * as i0 from '@angular/core'; import { forwardRef, ViewChild, Input, Component } from '@angular/core'; import { MatSelectionList, MatListOption } from '@angular/material/list'; import * as i1 from '@angular/forms'; import { FormControl, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatCard } from '@angular/material/card'; import { MatButton } from '@angular/material/button'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatInput } from '@angular/material/input'; import { isEqual } from 'lodash'; import { MatIcon } from '@angular/material/icon'; class AvDualListBox { formBuilder; sourceList = []; destinationObjectReference = []; displayProperty = ''; destinationList = []; originalSourceList = []; originalDestinationList = []; form; SourceList; DestinationList; onChange = () => { }; onTouched = () => { }; disabled = false; constructor(formBuilder) { this.formBuilder = formBuilder; this.form = this.formBuilder.group({ sourceListSearch: new FormControl(), destinationListSearch: new FormControl(), }); } ngOnInit() { this.form.controls['destinationListSearch'].disabled; } /** * Responds to changes in input-bound properties of the component. * Executes logic whenever the bound properties are updated. * * @param {SimpleChanges} changes - An object that contains the changes to the bound properties. Each property corresponds to a component input and contains its current and previous values. * @return {void} This method does not return a value. */ ngOnChanges(changes) { if (changes['sourceList']) { this.originalSourceList = [...this.sourceList]; this.resetAndFilterSourceList(); } } // // writeValue(value: T[]): void { // if (value) { // this.destinationList = this.getMappedDestinationValues(value); // this.originalDestinationList = [...this.destinationList]; // this.resetAndFilterSourceList(); // this.DestinationList.options.forEach(option => option._setSelected(true)); // } else { // this.destinationList = []; // } // this.onChange(this.destinationList); // } /** * Writes the provided value to the destination list, processes and maps the values accordingly, * and updates the source and destination lists. * * @param {T[]} value - The array of values to process and write to the destination list. If the value is null or undefined, the destination list is cleared. * @return {void} This method does not return any value. */ writeValue(value) { if (value) { this.destinationList = this.getMappedDestinationValues(value); this.originalDestinationList = [...this.destinationList]; this.resetAndFilterSourceList(); // Ensure DestinationList exists before accessing options setTimeout(() => { if (this.DestinationList) { this.DestinationList.options.forEach(option => option._setSelected(true)); } }); } else { this.destinationList = []; } this.onChange(this.destinationList); } /** * Maps and filters the input array based on the `destinationObjectReference`. * For each item in the `value` array, it retrieves a nested value determined * by the keys in the `destinationObjectReference`. Only non-null values are returned. * * @param {T[]} value - The array of items to be processed and filtered. * @return {T[]} - A filtered array containing only the mapped and non-null values. */ getMappedDestinationValues(value) { if (this.destinationObjectReference.length > 0) { return value.map(item => { const nestedValue = this.destinationObjectReference.reduce((acc, key) => acc ? acc[key] : undefined, item); return nestedValue ? nestedValue : null; }).filter((item) => item !== null); } return value; } resetAndFilterSourceList() { this.sourceList = this.originalSourceList.filter(item => !this.destinationList.some(destItem => isEqual(destItem, item))); } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } setDisabledState(isDisabled) { this.disabled = isDisabled; } /** * Moves selected items between the source list and the destination list based on the provided direction. * * @param {boolean} isToDestination - A boolean flag to determine the direction of movement. If true, moves items from source list to destination list. If false, moves items from destination list to source list. * @return {void} No return value. */ moveItems(isToDestination) { const selectedItems = (isToDestination ? this.SourceList : this.DestinationList).selectedOptions.selected.map(option => option.value); if (isToDestination) { this.sourceList = this.sourceList.filter(item => !selectedItems.includes(item)); this.destinationList.push(...selectedItems); this.originalDestinationList = [...this.destinationList]; } else { this.destinationList = this.destinationList.filter(item => !selectedItems.includes(item)); this.sourceList.push(...selectedItems); this.originalDestinationList = [...this.destinationList]; } this.onChange(this.destinationList); this.onTouched(); this.clearSelection(isToDestination); } /** * Moves all items between source and destination lists based on the specified direction. * * @param {boolean} isToDestination - Indicates the direction of the move. * If true, moves all items from the source list to the destination list. * If false, moves all items from the destination list back to the source list. * * @return {void} This method does not return any value. */ moveAllItems(isToDestination) { if (isToDestination) { this.destinationList.push(...this.sourceList); this.originalDestinationList = [...this.destinationList]; this.sourceList = []; } else { this.sourceList.push(...this.destinationList); this.destinationList = []; this.originalDestinationList = [...this.destinationList]; } this.onChange(this.destinationList); this.onTouched(); } /** * Clears the selection from either the source list or the destination list based on the provided parameter. * * @param {boolean} isToDestination - A flag indicating whether to clear the destination list (`true`) or source list (`false`). * @return {void} This method does not return a value. */ clearSelection(isToDestination) { if (isToDestination) { this.SourceList.selectedOptions.clear(); } else { this.DestinationList.selectedOptions.clear(); } } /** * Filters the original list based on the search value from the specified form control. * * @param {string} searchControlName - The name of the form control used to retrieve the search input. * @param {T[]} originalList - The original unfiltered list of items. * @param {T[]} targetList - The list to store the filtered results. It will reflect the filtered or full contents of the original list. * @return {T[]} - The filtered list of items matching the search input, or the original list if no input is entered. */ filterList(searchControlName, originalList, targetList) { const searchValue = this.form.controls[searchControlName]?.value?.toLowerCase() || ''; if (searchValue) { // Filter the original list based on the search value targetList = originalList.filter(item => item[this.displayProperty].toLowerCase().includes(searchValue.toLowerCase())); } else { // If search input is empty, show all items again targetList = [...originalList]; } return targetList; // Return the filtered or original list } /** * Filters the source list based on the provided search criteria and updates the source list. * * @return {void} This method does not return a value. It updates the sourceList property with the filtered results. */ filterSourceList() { this.sourceList = this.filterList('sourceListSearch', this.originalSourceList, this.sourceList); } /** * Filters the destination list based on the search criteria provided and updates the existing list. * * @return {void} Updates the `destinationList` property with the filtered results. */ filterDestinationList() { this.destinationList = this.filterList('destinationListSearch', this.originalDestinationList, this.destinationList); } /** * A lifecycle hook that is called after the component's view has been fully initialized. * This method ensures that all options in the DestinationList are marked as selected. * * @return {void} This method does not return a value. */ ngAfterViewInit() { if (this.DestinationList) { this.DestinationList.options.forEach(option => option._setSelected(true)); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AvDualListBox, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.2", type: AvDualListBox, isStandalone: true, selector: "av-dual-list-box", inputs: { sourceList: "sourceList", destinationObjectReference: "destinationObjectReference", displayProperty: "displayProperty" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AvDualListBox), multi: true } ], viewQueries: [{ propertyName: "SourceList", first: true, predicate: ["SourceList"], descendants: true }, { propertyName: "DestinationList", first: true, predicate: ["DestinationList"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<mat-card class=\"listbox-container\">\r\n <mat-card class=\"source-list\">\r\n <mat-selection-list #SourceList>\r\n <form [formGroup]=\"form\">\r\n <mat-form-field appearance=\"outline\" class=\"searchFields\">\r\n <mat-label>Search Here</mat-label>\r\n <input matInput formControlName=\"sourceListSearch\" (keyup)=\"filterSourceList()\">\r\n </mat-form-field>\r\n </form>\r\n @for (item of sourceList; track sourceList) {\r\n <mat-list-option [value]=\"item\">\r\n {{ item[displayProperty] || 'No Such Property' }}\r\n </mat-list-option>\r\n }\r\n </mat-selection-list>\r\n </mat-card>\r\n\r\n <mat-card class=\"button-panel\">\r\n <button mat-raised-button color=\"primary\" (click)=\"moveItems(true)\">\r\n <mat-icon>chevron_right</mat-icon>\r\n </button>\r\n <button mat-raised-button color=\"primary\" (click)=\"moveItems(false)\">\r\n <mat-icon>chevron_left</mat-icon>\r\n </button>\r\n <button mat-raised-button color=\"accent\" (click)=\"moveAllItems(true)\">\r\n <mat-icon>double_arrow</mat-icon> <!-- Use CSS to rotate if needed -->\r\n </button>\r\n <button mat-raised-button color=\"accent\" (click)=\"moveAllItems(false)\">\r\n <mat-icon class=\"flip-horizontal\">double_arrow</mat-icon>\r\n </button>\r\n </mat-card>\r\n\r\n\r\n <mat-card class=\"destination-list\">\r\n <mat-selection-list #DestinationList>\r\n <form [formGroup]=\"form\">\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Search Here</mat-label>\r\n <input matInput formControlName=\"destinationListSearch\" (keyup)=\"filterDestinationList()\">\r\n </mat-form-field>\r\n </form>\r\n @for (item of destinationList; track destinationList) {\r\n <mat-list-option [value]=\"item\">\r\n {{ item[displayProperty] || 'No Such Property' }}\r\n </mat-list-option>\r\n }\r\n </mat-selection-list>\r\n </mat-card>\r\n</mat-card>\r\n", styles: [":host .listbox-container{display:-webkit-box;align-items:center;padding:1%;box-shadow:unset;background:unset}:host .listbox-container .source-list,:host .listbox-container .destination-list{width:20%;height:260px;overflow:auto;background:#fdfbff}:is(:host .listbox-container .source-list,:host .listbox-container .destination-list) mat-form-field{background:#fdfbff}:host .button-panel{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:25px;box-shadow:none;background:none;margin:0 15px}:host .button-panel button{display:flex;justify-content:center;align-items:center;font-size:1rem;font-weight:500;color:#333;background:#fdfbff;width:fit-content;border-radius:10px;border:1px solid #818181}:host .button-panel button:hover{background:#3b81f4;color:#fff;border:1px solid #fdfdfe;cursor:pointer}:host .button-panel button mat-icon{display:flex;justify-content:center;align-items:center;margin:0}:host .flip-horizontal{transform:scaleX(-1)}:host ::-webkit-scrollbar{display:none}:host .source-list,:host .destination-list{border:#333333 1px solid;box-shadow:unset}:host mat-form-field{--mat-form-field-container-height: 40px;--mat-form-field-container-vertical-padding: 8px;width:95%;justify-items:center;position:sticky;top:4.1%;background:#fdfbff;z-index:1;left:2.5%}:host .center-input{text-align:center}\n"], dependencies: [{ kind: "component", type: MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: MatSelectionList, selector: "mat-selection-list", inputs: ["color", "compareWith", "multiple", "hideSingleSelectionIndicator", "disabled"], outputs: ["selectionChange"], exportAs: ["matSelectionList"] }, { kind: "component", type: MatListOption, selector: "mat-list-option", inputs: ["togglePosition", "checkboxPosition", "color", "value", "selected"], outputs: ["selectedChange"], exportAs: ["matListOption"] }, { kind: "component", type: MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AvDualListBox, decorators: [{ type: Component, args: [{ selector: 'av-dual-list-box', imports: [ MatCard, MatSelectionList, MatListOption, MatButton, ReactiveFormsModule, MatFormField, MatInput, MatLabel, MatIcon ], standalone: true, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AvDualListBox), multi: true } ], template: "<mat-card class=\"listbox-container\">\r\n <mat-card class=\"source-list\">\r\n <mat-selection-list #SourceList>\r\n <form [formGroup]=\"form\">\r\n <mat-form-field appearance=\"outline\" class=\"searchFields\">\r\n <mat-label>Search Here</mat-label>\r\n <input matInput formControlName=\"sourceListSearch\" (keyup)=\"filterSourceList()\">\r\n </mat-form-field>\r\n </form>\r\n @for (item of sourceList; track sourceList) {\r\n <mat-list-option [value]=\"item\">\r\n {{ item[displayProperty] || 'No Such Property' }}\r\n </mat-list-option>\r\n }\r\n </mat-selection-list>\r\n </mat-card>\r\n\r\n <mat-card class=\"button-panel\">\r\n <button mat-raised-button color=\"primary\" (click)=\"moveItems(true)\">\r\n <mat-icon>chevron_right</mat-icon>\r\n </button>\r\n <button mat-raised-button color=\"primary\" (click)=\"moveItems(false)\">\r\n <mat-icon>chevron_left</mat-icon>\r\n </button>\r\n <button mat-raised-button color=\"accent\" (click)=\"moveAllItems(true)\">\r\n <mat-icon>double_arrow</mat-icon> <!-- Use CSS to rotate if needed -->\r\n </button>\r\n <button mat-raised-button color=\"accent\" (click)=\"moveAllItems(false)\">\r\n <mat-icon class=\"flip-horizontal\">double_arrow</mat-icon>\r\n </button>\r\n </mat-card>\r\n\r\n\r\n <mat-card class=\"destination-list\">\r\n <mat-selection-list #DestinationList>\r\n <form [formGroup]=\"form\">\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Search Here</mat-label>\r\n <input matInput formControlName=\"destinationListSearch\" (keyup)=\"filterDestinationList()\">\r\n </mat-form-field>\r\n </form>\r\n @for (item of destinationList; track destinationList) {\r\n <mat-list-option [value]=\"item\">\r\n {{ item[displayProperty] || 'No Such Property' }}\r\n </mat-list-option>\r\n }\r\n </mat-selection-list>\r\n </mat-card>\r\n</mat-card>\r\n", styles: [":host .listbox-container{display:-webkit-box;align-items:center;padding:1%;box-shadow:unset;background:unset}:host .listbox-container .source-list,:host .listbox-container .destination-list{width:20%;height:260px;overflow:auto;background:#fdfbff}:is(:host .listbox-container .source-list,:host .listbox-container .destination-list) mat-form-field{background:#fdfbff}:host .button-panel{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:25px;box-shadow:none;background:none;margin:0 15px}:host .button-panel button{display:flex;justify-content:center;align-items:center;font-size:1rem;font-weight:500;color:#333;background:#fdfbff;width:fit-content;border-radius:10px;border:1px solid #818181}:host .button-panel button:hover{background:#3b81f4;color:#fff;border:1px solid #fdfdfe;cursor:pointer}:host .button-panel button mat-icon{display:flex;justify-content:center;align-items:center;margin:0}:host .flip-horizontal{transform:scaleX(-1)}:host ::-webkit-scrollbar{display:none}:host .source-list,:host .destination-list{border:#333333 1px solid;box-shadow:unset}:host mat-form-field{--mat-form-field-container-height: 40px;--mat-form-field-container-vertical-padding: 8px;width:95%;justify-items:center;position:sticky;top:4.1%;background:#fdfbff;z-index:1;left:2.5%}:host .center-input{text-align:center}\n"] }] }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { sourceList: [{ type: Input }], destinationObjectReference: [{ type: Input }], displayProperty: [{ type: Input }], SourceList: [{ type: ViewChild, args: ['SourceList'] }], DestinationList: [{ type: ViewChild, args: ['DestinationList'] }] } }); /* * Public API Surface of av-dual-list-box */ /** * Generated bundle index. Do not edit. */ export { AvDualListBox }; //# sourceMappingURL=avoraui-av-dual-list-box.mjs.map