Skip to content

Commit aeb09a8

Browse files
deantermbenjamincharity
authored andcommitted
fix(autocomplete): add comparator/formatter. fix things
1 parent d170c31 commit aeb09a8

File tree

6 files changed

+81
-66
lines changed

6 files changed

+81
-66
lines changed

demo/app/components/autocomplete/autocomplete.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ <h3 tsCardTitle tsVerticalSpacing="small--1">
1515
hint="Begin typing to select.."
1616
[formControl]="stateCtrl"
1717
[allowMultiple]="true"
18-
[allowDuplicateSelections]="true"
18+
[allowDuplicateSelections]="false"
1919
[reopenAfterSelection]="false"
2020
[showProgress]="fakeAsync"
21+
[displayFormatter]="formatter"
2122
(queryChange)="queryHasChanged($event)"
2223
(duplicateSelection)="duplicate($event)"
2324
tsVerticalSpacing
@@ -58,6 +59,7 @@ <h3 tsCardTitle tsVerticalSpacing="small--1">
5859
[allowDuplicateSelections]="true"
5960
[reopenAfterSelection]="false"
6061
[showProgress]="fakeAsync"
62+
[displayFormatter]="formatter"
6163
(queryChange)="queryHasChanged($event)"
6264
(duplicateSelection)="duplicate($event)"
6365
tsVerticalSpacing

demo/app/components/autocomplete/autocomplete.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class AutocompleteComponent implements OnInit {
116116
fakeAsync = false;
117117

118118
stateCtrl = new FormControl([this.states[4]], [Validators.required]);
119-
singleStateCtrl = new FormControl(null, [Validators.required]);
119+
singleStateCtrl = new FormControl([this.states[4]], [Validators.required]);
120120

121121
constructor() {
122122
this.filteredStates = this.myQuery$
@@ -149,4 +149,8 @@ export class AutocompleteComponent implements OnInit {
149149
duplicate(e) {
150150
console.log('DEMO: Duplicate selection: ', e);
151151
}
152+
153+
formatter(value: State): string {
154+
return value.name;
155+
}
152156
}

terminus-ui/autocomplete/src/autocomplete.component.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
<ng-container *ngIf="allowMultiple">
1818
<mat-chip-list #chipList="matChipList">
1919
<mat-chip
20-
*ngFor="let option of autocompleteSelections; trackBy: trackByFn"
20+
*ngFor="let option of autocompleteFormControl.value; trackBy: trackByFn"
2121
class="qa-autocomplete-chip"
2222
[removable]="true"
2323
(removed)="autocompleteDeselectItem(option)"
2424
>
2525
<span class="ts-autocomplete-chip-value">
26-
{{ option.viewValue }}
26+
{{ displayFormatter(option) }}
2727
</span>
2828

2929
<ts-icon matChipRemove>
@@ -39,7 +39,7 @@
3939
[attr.id]="id"
4040
[(ngModel)]="searchQuery"
4141
[readonly]="isDisabled ? 'true' : null"
42-
(ngModelChange)="querySubject.next(searchQuery)"
42+
(ngModelChange)="querySubject.next($event)"
4343
(blur)="handleInputBlur($event)"
4444
(keydown)="$event.stopPropagation()"
4545
#input
@@ -56,7 +56,8 @@
5656
[attr.id]="id"
5757
[readonly]="isDisabled ? 'true' : null"
5858
[(ngModel)]="searchQuery"
59-
(ngModelChange)="querySubject.next(searchQuery)"
59+
[value]="searchQuery"
60+
(ngModelChange)="querySubject.next($event)"
6061
(blur)="handleInputBlur($event)"
6162
#input
6263
/>

terminus-ui/autocomplete/src/autocomplete.component.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ By default, at least two characters must be typed before the query is fired. Thi
100100
</ts-autocomplete>
101101
```
102102

103+
## Formatting options
104+
```html
105+
<ts-autocomplete
106+
[formControl]="myCtrl"
107+
[displayFormatter]="formatDisplay"
108+
[valueComparator]="compareValues"
109+
>
110+
...
111+
</ts-autocomplete>
112+
```
113+
103114
## Test Helpers
104115

105116
Some helpers are exposed to assist with testing. These are imported from `@terminus/ui/autocomplete/testing`;

terminus-ui/autocomplete/src/autocomplete.component.spec.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,13 @@ describe(`TsAutocompleteComponent`, function() {
350350

351351
// Verify the selection DID work
352352
chips = getAllChipInstances(fixture);
353-
expect(chips.length).toEqual(1);
353+
expect(chips.length).toEqual(2);
354354
const autocomplete = getAutocompleteInstance(fixture);
355355
const option = autocomplete.options.find(o => o.value === states[4]);
356-
expect(autocomplete.autocompleteSelections).toEqual([option]);
357356
expect(autocomplete.autocompleteFormControl.value.length).toEqual(2);
358357
expect(autocomplete.autocompleteFormControl.value).toEqual([states[4], states[4]]);
359358

360-
expect.assertions(5);
359+
expect.assertions(4);
361360
}));
362361

363362
});
@@ -432,8 +431,6 @@ describe(`TsAutocompleteComponent`, function() {
432431

433432
const instance = getAutocompleteInstance(fixture);
434433
expect(instance.autocompleteFormControl.value).toEqual([states[4]]);
435-
expect(instance.autocompleteSelections.length).toEqual(1);
436-
expect(instance.autocompleteSelections[0]).toEqual(instance.options.find(opt => opt.value === states[4]));
437434
}));
438435

439436
test(`should allow a value seeded by a FormControl`, fakeAsync(function() {
@@ -710,5 +707,19 @@ describe(`TsAutocompleteComponent`, function() {
710707

711708
});
712709

710+
test('onContainerClick', () => {
711+
const fixture = createComponent(testComponents.SeededAutocomplete);
712+
const autocomplete = getAutocompleteInstance(fixture);
713+
autocomplete.focus = jest.fn();
714+
autocomplete.onContainerClick();
715+
expect(autocomplete.focus).toHaveBeenCalled();
716+
});
717+
718+
test('value getter/setter', () => {
719+
const fixture = createComponent(testComponents.SeededAutocomplete);
720+
const autocomplete = getAutocompleteInstance(fixture);
721+
autocomplete.value = 'testing';
722+
expect(autocomplete.value).toEqual('testing');
723+
});
713724

714725
});

terminus-ui/autocomplete/src/autocomplete.component.ts

Lines changed: 41 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// tslint:disable: template-no-call-expression
33
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
44
import {
5-
AfterContentInit,
65
AfterViewInit,
76
ChangeDetectionStrategy,
87
ChangeDetectorRef,
@@ -71,13 +70,16 @@ const DEFAULT_DEBOUNCE_DELAY = 200;
7170
/**
7271
* The event object that is emitted when the select value has changed
7372
*/
74-
export class TsAutocompleteChange<T = string[] | string> {
73+
export class TsAutocompleteChange<T = unknown> {
7574
constructor(
7675
public source: TsAutocompleteComponent,
7776
public value: T,
7877
) { }
7978
}
8079

80+
export type TsAutocompleteFormatter = (v: unknown) => string;
81+
export type TsAutocompleteComparator = (a: unknown, b: unknown) => boolean;
82+
8183
/**
8284
* The autocomplete UI Component
8385
*
@@ -94,6 +96,8 @@ export class TsAutocompleteChange<T = string[] | string> {
9496
* @example
9597
* <ts-autocomplete
9698
* [allowMultiple]="allowMultiple"
99+
* [displayFormatter]="formatterFunc"
100+
* [valueComparator]="comparatorFunc"
97101
* debounceDelay="300"
98102
* displayWith="(v) => v.name"
99103
* hint="Begin typing to search.."
@@ -141,7 +145,6 @@ export class TsAutocompleteChange<T = string[] | string> {
141145
})
142146
export class TsAutocompleteComponent implements OnInit,
143147
AfterViewInit,
144-
AfterContentInit,
145148
OnDestroy,
146149
TsFormFieldControl<string> {
147150

@@ -155,11 +158,6 @@ export class TsAutocompleteComponent implements OnInit,
155158
*/
156159
public autocompleteFormControl = new FormControl([]);
157160

158-
/**
159-
* An array of selected values
160-
*/
161-
public autocompleteSelections: TsOptionComponent[] = [];
162-
163161
/**
164162
* Store a reference to the document object
165163
*/
@@ -417,6 +415,19 @@ export class TsAutocompleteComponent implements OnInit,
417415
@Input()
418416
public name: string | undefined;
419417

418+
/**
419+
* Define the formatter for the selected items.
420+
*/
421+
@Input()
422+
public displayFormatter: TsAutocompleteFormatter = v => v as string
423+
424+
/**
425+
* Define the comparator for the values of the options
426+
*/
427+
@Input()
428+
public valueComparator: TsAutocompleteComparator = (a: unknown, b: unknown) => a === b
429+
430+
420431
/**
421432
* Event for when the panel is closed
422433
*/
@@ -508,7 +519,9 @@ export class TsAutocompleteComponent implements OnInit,
508519
throw Error('form control values must be an array of values');
509520
} else if (this.ngControl.value) {
510521
this.autocompleteFormControl.setValue(this.ngControl.value);
511-
this.setSelections();
522+
if (!this.allowMultiple) {
523+
this.searchQuery = this.displayFormatter(this.ngControl.value[0]);
524+
}
512525
}
513526

514527
// Support dynamic form control updates
@@ -520,7 +533,9 @@ export class TsAutocompleteComponent implements OnInit,
520533
// istanbul ignore else
521534
if (newValue) {
522535
this.autocompleteFormControl.setValue(newValue, { emitEvent: false });
523-
this.setSelections();
536+
if (!this.allowMultiple) {
537+
this.searchQuery = this.displayFormatter(newValue[0]);
538+
}
524539
}
525540
});
526541
}
@@ -534,7 +549,9 @@ export class TsAutocompleteComponent implements OnInit,
534549
throw Error('ngModel must be an array of values');
535550
}
536551
this.autocompleteFormControl.setValue(this.ngControl.value);
537-
this.setSelections();
552+
if (!this.allowMultiple) {
553+
this.searchQuery = this.displayFormatter(this.ngControl.value[0]);
554+
}
538555
}
539556
});
540557
}
@@ -590,13 +607,6 @@ export class TsAutocompleteComponent implements OnInit,
590607

591608
}
592609

593-
public ngAfterContentInit(): void {
594-
this.setSelections();
595-
this.options.changes
596-
.pipe(untilComponentDestroyed(this))
597-
.subscribe(() => this.setSelections());
598-
}
599-
600610
/**
601611
* Needed for untilComponentDestroyed
602612
*/
@@ -793,8 +803,8 @@ export class TsAutocompleteComponent implements OnInit,
793803
* @param selection - The item to select
794804
*/
795805
public autocompleteSelectItem(selection: TsAutocompletePanelSelectedEvent): void {
796-
const isDuplicate = this.autocompleteSelections
797-
.findIndex(s => selection.option.value === s.value) >= 0;
806+
const isDuplicate = (this.autocompleteFormControl.value || [])
807+
.findIndex(o => this.valueComparator(o, selection.option.value)) >= 0;
798808

799809
// istanbul ignore else
800810
if (isDuplicate) {
@@ -814,17 +824,12 @@ export class TsAutocompleteComponent implements OnInit,
814824
this.resetAutocompleteQuery();
815825
}
816826

817-
// Add to the collection
818-
this.autocompleteSelections = this.autocompleteSelections.concat(selection.option);
819-
820827
// Update the form control
821-
this.autocompleteFormControl.setValue(this.autocompleteSelections.map(s => s.value));
828+
const options = (this.autocompleteFormControl.value || []).concat(selection.option.value);
829+
this.autocompleteFormControl.setValue(options);
822830
} else {
823-
// Update the selected value
824-
this.autocompleteSelections = [selection.option];
825-
826831
// Update the form control
827-
this.autocompleteFormControl.setValue(this.autocompleteSelections.map(s => s.value));
832+
this.autocompleteFormControl.setValue([selection.option.value]);
828833

829834
// In single selection mode, set the query input to the selection so the user can see what was selected
830835
this.inputElement.nativeElement.value = selection.option.viewValue;
@@ -839,7 +844,7 @@ export class TsAutocompleteComponent implements OnInit,
839844

840845
// Notify consumers about changes
841846
this.optionSelected.emit(new TsAutocompleteChange(this, selection.option.value));
842-
this.selectionChange.emit(new TsAutocompleteChange(this, this.autocompleteSelections.map(s => s.value)));
847+
this.selectionChange.emit(new TsAutocompleteChange(this, this.autocompleteFormControl.value));
843848
}
844849

845850

@@ -848,25 +853,17 @@ export class TsAutocompleteComponent implements OnInit,
848853
*
849854
* @param value - The value of the item to remove
850855
*/
851-
public autocompleteDeselectItem(option: TsOptionComponent): void {
856+
public autocompleteDeselectItem(option: unknown): void {
852857
// Find the key of the selection in the selectedOptions array
853-
const index = this.autocompleteSelections.findIndex(s => s.value === option.value);
854-
const selections = this.autocompleteSelections.slice();
855-
// If not found
856-
if (index < 0) {
857-
return;
858-
}
859-
860-
// Remove the selection from the selectedOptions array
861-
selections.splice(index, 1);
862-
this.autocompleteSelections = selections;
858+
const options = (this.autocompleteFormControl.value || [])
859+
.filter(opt => !this.valueComparator(opt, option));
863860

864861
// Update the form control
865-
this.autocompleteFormControl.setValue(this.autocompleteSelections.map(s => s.value));
862+
this.autocompleteFormControl.setValue(options);
866863

867864
// If the only chip was removed, re-focus the input
868865
// istanbul ignore else
869-
if (this.autocompleteSelections.length < 1) {
866+
if (options.length === 0) {
870867
this.focus();
871868
}
872869

@@ -880,8 +877,8 @@ export class TsAutocompleteComponent implements OnInit,
880877
});
881878

882879
// Notify consumers about changes
883-
this.optionDeselected.emit(new TsAutocompleteChange(this, option.value));
884-
this.selectionChange.emit(new TsAutocompleteChange(this, this.autocompleteSelections.map(s => s.value)));
880+
this.optionDeselected.emit(new TsAutocompleteChange(this, option));
881+
this.selectionChange.emit(new TsAutocompleteChange(this, options));
885882
}
886883

887884

@@ -894,15 +891,4 @@ export class TsAutocompleteComponent implements OnInit,
894891
public trackByFn(index): number {
895892
return index;
896893
}
897-
898-
/**
899-
* Finds the options that have been selected.
900-
*/
901-
private setSelections(): void {
902-
if (this.ngControl && this.ngControl.value && this.options) {
903-
this.autocompleteSelections = this.options.filter(opt => this.ngControl.value.indexOf(opt.value) >= 0);
904-
this.changeDetectorRef.detectChanges();
905-
}
906-
}
907-
908894
}

0 commit comments

Comments
 (0)