@avoraui/av-dual-list-box
Version:
A customizable Angular dual list box component for managing item selections.
273 lines (268 loc) • 21.5 kB
JavaScript
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