From e8409a59fec704f81585aa23f91fb7893571e71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=C3=A9fling?= Date: Sat, 7 Dec 2024 01:16:55 +0100 Subject: [PATCH] Rewrite value change without setTimeout - fix resettable with selectionOverride - removal of unnecessary cycles that complicate debugging when writing the value - fix weird value update cases --- .../src/lib/select2-utils.ts | 3 +- .../src/lib/select2.component.html | 15 +- .../src/lib/select2.component.ts | 217 ++++++++++-------- src/app/app-examples.component.html | 14 +- src/app/app-examples.component.ts | 4 +- src/app/app-gen.component.html | 65 +++--- 6 files changed, 175 insertions(+), 143 deletions(-) diff --git a/projects/ng-select2-component/src/lib/select2-utils.ts b/projects/ng-select2-component/src/lib/select2-utils.ts index a5ec315..5e690f9 100644 --- a/projects/ng-select2-component/src/lib/select2-utils.ts +++ b/projects/ng-select2-component/src/lib/select2-utils.ts @@ -1,7 +1,6 @@ import { defaultMinCountForSearch, protectRegexp, unicodePatterns } from './select2-const'; import { Select2Data, Select2Group, Select2Option, Select2UpdateValue, Select2Value } from './select2-interfaces'; - export class Select2Utils { static getOptionByValue(data: Select2Data, value: Select2Value | null | undefined) { if (Array.isArray(data)) { @@ -224,7 +223,7 @@ export class Select2Utils { return result; } - static isSearchboxHiddex(data: Select2Data, minCountForSearch?: number): boolean { + static isSearchboxHidden(data: Select2Data, minCountForSearch?: number): boolean { if (minCountForSearch === undefined || minCountForSearch === null || isNaN(+minCountForSearch)) { minCountForSearch = defaultMinCountForSearch; } diff --git a/projects/ng-select2-component/src/lib/select2.component.html b/projects/ng-select2-component/src/lib/select2.component.html index 7e0678e..8b44f3d 100644 --- a/projects/ng-select2-component/src/lib/select2.component.html +++ b/projects/ng-select2-component/src/lib/select2.component.html @@ -33,6 +33,10 @@ > @if (selectionOverride()) { + + @if (resettable() && resetSelectedValue() !== value() && select2Option && !(disabled() || readonly())) { + × + } @if ( !multiple() && resettable() && resetSelectedValue() !== value() && select2Option && !(disabled || readonly()) ) { @@ -56,7 +60,8 @@ placeholder() }} - @if (resettable() && resetSelectedValue() !== value() && select2Option && !(disabled || readonly())) { + + @if (resettable() && resetSelectedValue() !== value() && select2Option && !(disabled() || readonly())) { × } @@ -94,7 +99,7 @@ @if (autoCreate()) {
  • (); readonly minCharForSearch = input(0, { transform: numberAttribute }); @@ -95,7 +128,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView readonly customSearchEnabled = input(false, { transform: booleanAttribute }); /** minimal data of show the search field */ - readonly minCountForSearch = input(0, { transform: numberAttribute }); + readonly minCountForSearch = input(undefined, { transform: numberAttribute }); /** Unique id of the element. */ readonly id = input(); @@ -221,7 +254,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView return this.innerSearchText; } - set searchText(text: string) { + protected set searchText(text: string) { this.innerSearchText = text; } @@ -229,18 +262,16 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView return this._control?.disabled ?? this._disabled; } - overlayWidth: number; - overlayHeight: number; - _triggerRect: DOMRect; - _dropdownRect: DOMRect; + protected overlayWidth: number; + protected overlayHeight: number; + protected _triggerRect: DOMRect; + protected _dropdownRect: DOMRect; - get _positions(): ConnectedPosition[] { + protected get _positions(): ConnectedPosition[] { return this.listPosition() === 'auto' ? undefined : null; } - maxResultsExceeded: boolean; - - private _minCountForSearch?: number; + protected maxResultsExceeded: boolean; private hoveringValue: Select2Value | null | undefined = null; private innerSearchText = ''; @@ -289,11 +320,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView ); this.toObservable.add( toObservable(this.data).subscribe(_data => { - this.updateFilteredData(true); + this.updateFilteredData(); }), ); this.toObservable.add( - toObservable(this.minCountForSearch).subscribe(_minCountForSearch => { + toObservable(this.minCountForSearch).subscribe(minCountForSearch => { this.updateSearchBox(); }), ); @@ -310,12 +341,10 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.toObservable.add( toObservable(this.value).subscribe(value => { if (this.testValueChange(this._value, value)) { - setTimeout(() => { - if (this._value === undefined) { - this._value = value ?? null; - } - this.writeValue(value ?? null); - }, 10); + if (this._value === undefined) { + this._value = value ?? null; + } + this.writeValue(value ?? null); } }), ); @@ -410,7 +439,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView updateSearchBox() { const hidden = this.customSearchEnabled() ? false - : Select2Utils.isSearchboxHiddex(this.data(), this._minCountForSearch); + : Select2Utils.isSearchboxHidden(this.data(), this.minCountForSearch()); if (this.isSearchboxHidden !== hidden) { this.isSearchboxHidden = hidden; } @@ -635,70 +664,63 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } private updateFilteredData(writeValue = false) { - setTimeout(() => { - let result = this.data(); - if (this.multiple() && this.hideSelectedItems()) { - result = Select2Utils.getFilteredSelectedData(result, this.option); - } - - if (!this.customSearchEnabled() && this.searchText && this.searchText.length >= +this.minCharForSearch()) { - result = Select2Utils.getFilteredData(result, this.searchText, this.editPattern()); - } + let result = this.data(); + if (this.multiple() && this.hideSelectedItems()) { + result = Select2Utils.getFilteredSelectedData(result, this.option); + } - if (this.maxResults() > 0) { - const data = Select2Utils.getReduceData(result, +this.maxResults()); - result = data.result; - this.maxResultsExceeded = data.reduce; - } else { - this.maxResultsExceeded = false; - } + if (!this.customSearchEnabled() && this.searchText && this.searchText.length >= +this.minCharForSearch()) { + result = Select2Utils.getFilteredData(result, this.searchText, this.editPattern()); + } - if (Select2Utils.valueIsNotInFilteredData(result, this.hoveringValue)) { - this.hoveringValue = Select2Utils.getFirstAvailableOption(result); - } + if (this.maxResults() > 0) { + const data = Select2Utils.getReduceData(result, +this.maxResults()); + result = data.result; + this.maxResultsExceeded = data.reduce; + } else { + this.maxResultsExceeded = false; + } - if (writeValue && this._previousNativeValue !== this._value) { - // refresh current selected value - this.writeValue(this._control ? this._control.value : this._value); - } + if (Select2Utils.valueIsNotInFilteredData(result, this.hoveringValue)) { + this.hoveringValue = Select2Utils.getFirstAvailableOption(result); + } - this.filteredData.set(result); + this.filteredData.set(result); - // replace selected options when data change + // replace selected options when data change - if (this.multiple() && Array.isArray(this.option) && this.option.length) { - const options: Select2Option[] = []; - const value = this.option.map(e => e.value); - this.data().forEach(e => { - if ((e as Select2Group).options) { - (e as Select2Group).options.forEach(f => { - if (value.includes(f.value)) { - options.push(f); - } - }); - } else if (value.includes((e as Select2Option).value)) { - options.push(e as Select2Option); - } - }); - // preserve selection order - this.option = this.option.map(e => options.find(f => f.value === e.value)); - } else if (!Array.isArray(this.option) && this.option) { - let option: Select2Option = undefined; - this.data().forEach(e => { - if ((e as Select2Group).options) { - (e as Select2Group).options.forEach(f => { - if ((this.option as Select2Option).value === f.value) { - option = f; - } - }); - } else if ((this.option as Select2Option).value === (e as Select2Option).value) { - option = e as Select2Option; - } - }); - this.option = option; - } - this._changeDetectorRef.detectChanges(); - }); + if (this.multiple() && Array.isArray(this.option) && this.option.length) { + const options: Select2Option[] = []; + const value = this.option.map(e => e.value); + this.data().forEach(e => { + if ((e as Select2Group).options) { + (e as Select2Group).options.forEach(f => { + if (value.includes(f.value)) { + options.push(f); + } + }); + } else if (value.includes((e as Select2Option).value)) { + options.push(e as Select2Option); + } + }); + // preserve selection order + this.option = this.option.map(e => options.find(f => f.value === e.value)); + } else if (!Array.isArray(this.option) && this.option) { + let option: Select2Option = undefined; + this.data().forEach(e => { + if ((e as Select2Group).options) { + (e as Select2Group).options.forEach(f => { + if ((this.option as Select2Option).value === f.value) { + option = f; + } + }); + } else if ((this.option as Select2Option).value === (e as Select2Option).value) { + option = e as Select2Option; + } + }); + this.option = option; + } + this._changeDetectorRef.detectChanges(); } private clickExit() { @@ -784,6 +806,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView let value: any; if (option !== null && option !== undefined) { if (this.multiple()) { + this.option ??= []; const options = this.option as Select2Option[]; const index = options.findIndex(op => op.value === option.value); if (index === -1) { @@ -943,8 +966,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView * @param value */ writeValue(value: any) { + this.option = null; this._setSelectionByValue(value); - this._value = value; + if (this.testValueChange(this._value, value)) { + this._value = value; + } } /** @@ -1100,11 +1126,14 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView throw new Error('Non array value.'); } else if (this.data()) { if (this.multiple()) { - this.option = []; // if value is null, then empty option and return + if (!Array.isArray(this.option)) { + this.option = []; // if value is null, then empty option and return + } if (isArray) { // value is not null. Preselect value - const selectedValues: any = Select2Utils.getOptionsByValue(this.data(), value, this.multiple()); - selectedValues.map(item => this.select(item, false)); + (Select2Utils.getOptionsByValue(this.data(), value, this.multiple()) as []).forEach(item => + this.select(item, false), + ); this._value ??= value; if (this.testDiffValue(this._value, value)) { @@ -1130,8 +1159,8 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } } } else { - this._value ??= value; - this.select(Select2Utils.getOptionByValue(this.data(), value)); + this._value = value; + this.select(Select2Utils.getOptionByValue(this.data(), this._value)); } } else if (this._control) { this._control.viewToModelUpdate(value); @@ -1181,4 +1210,4 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView ? this._overlayPosition === 'top' : listPosition === 'above'; } -} \ No newline at end of file +} diff --git a/src/app/app-examples.component.html b/src/app/app-examples.component.html index 1c50401..cac02d8 100644 --- a/src/app/app-examples.component.html +++ b/src/app/app-examples.component.html @@ -29,7 +29,7 @@

    3. less options ({{ value3 }})

    4. disabled ({{ value4 }})

    -

    5. hide search box ({{ value5 }})

    +

    5. search box (infinity) ({{ value5 }})

    5. hide search box ({{ value5 }})

    - 6. search limit to / display status + 6. search box limit to / display status +
    - +
    @@ -232,39 +232,38 @@

    Events

    HTML render

    @if (ctrlForm.value) { + @let value = ctrlForm.value;