Skip to content

Commit

Permalink
Showing 6 changed files with 81 additions and 66 deletions.
4 changes: 3 additions & 1 deletion demo/app/components/autocomplete/autocomplete.component.html
Original file line number Diff line number Diff line change
@@ -15,9 +15,10 @@ <h3 tsCardTitle tsVerticalSpacing="small--1">
hint="Begin typing to select.."
[formControl]="stateCtrl"
[allowMultiple]="true"
[allowDuplicateSelections]="true"
[allowDuplicateSelections]="false"
[reopenAfterSelection]="false"
[showProgress]="fakeAsync"
[displayFormatter]="formatter"
(queryChange)="queryHasChanged($event)"
(duplicateSelection)="duplicate($event)"
tsVerticalSpacing
@@ -58,6 +59,7 @@ <h3 tsCardTitle tsVerticalSpacing="small--1">
[allowDuplicateSelections]="true"
[reopenAfterSelection]="false"
[showProgress]="fakeAsync"
[displayFormatter]="formatter"
(queryChange)="queryHasChanged($event)"
(duplicateSelection)="duplicate($event)"
tsVerticalSpacing
6 changes: 5 additions & 1 deletion demo/app/components/autocomplete/autocomplete.component.ts
Original file line number Diff line number Diff line change
@@ -116,7 +116,7 @@ export class AutocompleteComponent implements OnInit {
fakeAsync = false;

stateCtrl = new FormControl([this.states[4]], [Validators.required]);
singleStateCtrl = new FormControl(null, [Validators.required]);
singleStateCtrl = new FormControl([this.states[4]], [Validators.required]);

constructor() {
this.filteredStates = this.myQuery$
@@ -149,4 +149,8 @@ export class AutocompleteComponent implements OnInit {
duplicate(e) {
console.log('DEMO: Duplicate selection: ', e);
}

formatter(value: State): string {
return value.name;
}
}
9 changes: 5 additions & 4 deletions terminus-ui/autocomplete/src/autocomplete.component.html
Original file line number Diff line number Diff line change
@@ -17,13 +17,13 @@
<ng-container *ngIf="allowMultiple">
<mat-chip-list #chipList="matChipList">
<mat-chip
*ngFor="let option of autocompleteSelections; trackBy: trackByFn"
*ngFor="let option of autocompleteFormControl.value; trackBy: trackByFn"
class="qa-autocomplete-chip"
[removable]="true"
(removed)="autocompleteDeselectItem(option)"
>
<span class="ts-autocomplete-chip-value">
{{ option.viewValue }}
{{ displayFormatter(option) }}
</span>

<ts-icon matChipRemove>
@@ -39,7 +39,7 @@
[attr.id]="id"
[(ngModel)]="searchQuery"
[readonly]="isDisabled ? 'true' : null"
(ngModelChange)="querySubject.next(searchQuery)"
(ngModelChange)="querySubject.next($event)"
(blur)="handleInputBlur($event)"
(keydown)="$event.stopPropagation()"
#input
@@ -56,7 +56,8 @@
[attr.id]="id"
[readonly]="isDisabled ? 'true' : null"
[(ngModel)]="searchQuery"
(ngModelChange)="querySubject.next(searchQuery)"
[value]="searchQuery"
(ngModelChange)="querySubject.next($event)"
(blur)="handleInputBlur($event)"
#input
/>
11 changes: 11 additions & 0 deletions terminus-ui/autocomplete/src/autocomplete.component.md
Original file line number Diff line number Diff line change
@@ -100,6 +100,17 @@ By default, at least two characters must be typed before the query is fired. Thi
</ts-autocomplete>
```

## Formatting options
```html
<ts-autocomplete
[formControl]="myCtrl"
[displayFormatter]="formatDisplay"
[valueComparator]="compareValues"
>
...
</ts-autocomplete>
```

## Test Helpers

Some helpers are exposed to assist with testing. These are imported from `@terminus/ui/autocomplete/testing`;
21 changes: 16 additions & 5 deletions terminus-ui/autocomplete/src/autocomplete.component.spec.ts
Original file line number Diff line number Diff line change
@@ -350,14 +350,13 @@ describe(`TsAutocompleteComponent`, function() {

// Verify the selection DID work
chips = getAllChipInstances(fixture);
expect(chips.length).toEqual(1);
expect(chips.length).toEqual(2);
const autocomplete = getAutocompleteInstance(fixture);
const option = autocomplete.options.find(o => o.value === states[4]);
expect(autocomplete.autocompleteSelections).toEqual([option]);
expect(autocomplete.autocompleteFormControl.value.length).toEqual(2);
expect(autocomplete.autocompleteFormControl.value).toEqual([states[4], states[4]]);

expect.assertions(5);
expect.assertions(4);
}));

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

const instance = getAutocompleteInstance(fixture);
expect(instance.autocompleteFormControl.value).toEqual([states[4]]);
expect(instance.autocompleteSelections.length).toEqual(1);
expect(instance.autocompleteSelections[0]).toEqual(instance.options.find(opt => opt.value === states[4]));
}));

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

});

test('onContainerClick', () => {
const fixture = createComponent(testComponents.SeededAutocomplete);
const autocomplete = getAutocompleteInstance(fixture);
autocomplete.focus = jest.fn();
autocomplete.onContainerClick();
expect(autocomplete.focus).toHaveBeenCalled();
});

test('value getter/setter', () => {
const fixture = createComponent(testComponents.SeededAutocomplete);
const autocomplete = getAutocompleteInstance(fixture);
autocomplete.value = 'testing';
expect(autocomplete.value).toEqual('testing');
});

});
96 changes: 41 additions & 55 deletions terminus-ui/autocomplete/src/autocomplete.component.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
// tslint:disable: template-no-call-expression
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import {
AfterContentInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
@@ -71,13 +70,16 @@ const DEFAULT_DEBOUNCE_DELAY = 200;
/**
* The event object that is emitted when the select value has changed
*/
export class TsAutocompleteChange<T = string[] | string> {
export class TsAutocompleteChange<T = unknown> {
constructor(
public source: TsAutocompleteComponent,
public value: T,
) { }
}

export type TsAutocompleteFormatter = (v: unknown) => string;
export type TsAutocompleteComparator = (a: unknown, b: unknown) => boolean;

/**
* The autocomplete UI Component
*
@@ -94,6 +96,8 @@ export class TsAutocompleteChange<T = string[] | string> {
* @example
* <ts-autocomplete
* [allowMultiple]="allowMultiple"
* [displayFormatter]="formatterFunc"
* [valueComparator]="comparatorFunc"
* debounceDelay="300"
* displayWith="(v) => v.name"
* hint="Begin typing to search.."
@@ -141,7 +145,6 @@ export class TsAutocompleteChange<T = string[] | string> {
})
export class TsAutocompleteComponent implements OnInit,
AfterViewInit,
AfterContentInit,
OnDestroy,
TsFormFieldControl<string> {

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

/**
* An array of selected values
*/
public autocompleteSelections: TsOptionComponent[] = [];

/**
* Store a reference to the document object
*/
@@ -417,6 +415,19 @@ export class TsAutocompleteComponent implements OnInit,
@Input()
public name: string | undefined;

/**
* Define the formatter for the selected items.
*/
@Input()
public displayFormatter: TsAutocompleteFormatter = v => v as string

/**
* Define the comparator for the values of the options
*/
@Input()
public valueComparator: TsAutocompleteComparator = (a: unknown, b: unknown) => a === b


/**
* Event for when the panel is closed
*/
@@ -508,7 +519,9 @@ export class TsAutocompleteComponent implements OnInit,
throw Error('form control values must be an array of values');
} else if (this.ngControl.value) {
this.autocompleteFormControl.setValue(this.ngControl.value);
this.setSelections();
if (!this.allowMultiple) {
this.searchQuery = this.displayFormatter(this.ngControl.value[0]);
}
}

// Support dynamic form control updates
@@ -520,7 +533,9 @@ export class TsAutocompleteComponent implements OnInit,
// istanbul ignore else
if (newValue) {
this.autocompleteFormControl.setValue(newValue, { emitEvent: false });
this.setSelections();
if (!this.allowMultiple) {
this.searchQuery = this.displayFormatter(newValue[0]);
}
}
});
}
@@ -534,7 +549,9 @@ export class TsAutocompleteComponent implements OnInit,
throw Error('ngModel must be an array of values');
}
this.autocompleteFormControl.setValue(this.ngControl.value);
this.setSelections();
if (!this.allowMultiple) {
this.searchQuery = this.displayFormatter(this.ngControl.value[0]);
}
}
});
}
@@ -590,13 +607,6 @@ export class TsAutocompleteComponent implements OnInit,

}

public ngAfterContentInit(): void {
this.setSelections();
this.options.changes
.pipe(untilComponentDestroyed(this))
.subscribe(() => this.setSelections());
}

/**
* Needed for untilComponentDestroyed
*/
@@ -793,8 +803,8 @@ export class TsAutocompleteComponent implements OnInit,
* @param selection - The item to select
*/
public autocompleteSelectItem(selection: TsAutocompletePanelSelectedEvent): void {
const isDuplicate = this.autocompleteSelections
.findIndex(s => selection.option.value === s.value) >= 0;
const isDuplicate = (this.autocompleteFormControl.value || [])
.findIndex(o => this.valueComparator(o, selection.option.value)) >= 0;

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

// Add to the collection
this.autocompleteSelections = this.autocompleteSelections.concat(selection.option);

// Update the form control
this.autocompleteFormControl.setValue(this.autocompleteSelections.map(s => s.value));
const options = (this.autocompleteFormControl.value || []).concat(selection.option.value);
this.autocompleteFormControl.setValue(options);
} else {
// Update the selected value
this.autocompleteSelections = [selection.option];

// Update the form control
this.autocompleteFormControl.setValue(this.autocompleteSelections.map(s => s.value));
this.autocompleteFormControl.setValue([selection.option.value]);

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

// Notify consumers about changes
this.optionSelected.emit(new TsAutocompleteChange(this, selection.option.value));
this.selectionChange.emit(new TsAutocompleteChange(this, this.autocompleteSelections.map(s => s.value)));
this.selectionChange.emit(new TsAutocompleteChange(this, this.autocompleteFormControl.value));
}


@@ -848,25 +853,17 @@ export class TsAutocompleteComponent implements OnInit,
*
* @param value - The value of the item to remove
*/
public autocompleteDeselectItem(option: TsOptionComponent): void {
public autocompleteDeselectItem(option: unknown): void {
// Find the key of the selection in the selectedOptions array
const index = this.autocompleteSelections.findIndex(s => s.value === option.value);
const selections = this.autocompleteSelections.slice();
// If not found
if (index < 0) {
return;
}

// Remove the selection from the selectedOptions array
selections.splice(index, 1);
this.autocompleteSelections = selections;
const options = (this.autocompleteFormControl.value || [])
.filter(opt => !this.valueComparator(opt, option));

// Update the form control
this.autocompleteFormControl.setValue(this.autocompleteSelections.map(s => s.value));
this.autocompleteFormControl.setValue(options);

// If the only chip was removed, re-focus the input
// istanbul ignore else
if (this.autocompleteSelections.length < 1) {
if (options.length === 0) {
this.focus();
}

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

// Notify consumers about changes
this.optionDeselected.emit(new TsAutocompleteChange(this, option.value));
this.selectionChange.emit(new TsAutocompleteChange(this, this.autocompleteSelections.map(s => s.value)));
this.optionDeselected.emit(new TsAutocompleteChange(this, option));
this.selectionChange.emit(new TsAutocompleteChange(this, options));
}


@@ -894,15 +891,4 @@ export class TsAutocompleteComponent implements OnInit,
public trackByFn(index): number {
return index;
}

/**
* Finds the options that have been selected.
*/
private setSelections(): void {
if (this.ngControl && this.ngControl.value && this.options) {
this.autocompleteSelections = this.options.filter(opt => this.ngControl.value.indexOf(opt.value) >= 0);
this.changeDetectorRef.detectChanges();
}
}

}

0 comments on commit aeb09a8

Please sign in to comment.