Skip to content

Commit

Permalink
feat(Autocomplete): Autocomplete now works with ChipList
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Kimball committed Jun 24, 2022
1 parent 73b8aba commit 94454c6
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 114 deletions.
14 changes: 11 additions & 3 deletions projects/novo-elements/src/elements/chips/ChipInput.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { hasModifierKey } from '@angular/cdk/keycodes';
import { Directive, ElementRef, EventEmitter, forwardRef, Inject, Input, OnChanges, Output } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Key } from '../../utils';
import { NovoChipsDefaultOptions, NOVO_CHIPS_DEFAULT_OPTIONS } from './ChipDefaults';
import { NovoChipList } from './ChipList';
Expand All @@ -19,8 +20,8 @@ export interface NovoChipInputEvent {
let nextUniqueId = 0;

/**
* Directive that adds chip-specific behaviors to an input element inside `<mat-form-field>`.
* May be placed inside or outside of an `<mat-chip-list>`.
* Directive that adds chip-specific behaviors to an input element inside `<novo-form-field>`.
* May be placed inside or outside of an `<novo-chip-list>`.
*/
@Directive({
selector: 'input[novoChipInput]',
Expand Down Expand Up @@ -70,7 +71,7 @@ export class NovoChipInput implements NovoChipTextControl, OnChanges {
@Input() placeholder: string = '';

/** Unique id for the input. */
@Input() id: string = `mat-chip-list-input-${nextUniqueId++}`;
@Input() id: string = `novo-chip-list-input-${nextUniqueId++}`;

/** Whether the input is disabled. */
@Input()
Expand All @@ -94,6 +95,7 @@ export class NovoChipInput implements NovoChipTextControl, OnChanges {
protected _elementRef: ElementRef<HTMLInputElement>,
@Inject(NOVO_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: NovoChipsDefaultOptions,
@Inject(forwardRef(() => NovoChipList)) private _chipList: NovoChipList,
protected ngControl: NgControl,
) {
this._inputElement = this._elementRef.nativeElement as HTMLInputElement;
this._chipList.registerInput(this);
Expand Down Expand Up @@ -156,6 +158,12 @@ export class NovoChipInput implements NovoChipTextControl, OnChanges {
this._inputElement.focus(options);
}

/** Focuses the input. */
clearValue(): void {
this._inputElement.value = '';
this.ngControl?.control.setValue('');
}

/** Checks whether a keycode is one of the configured separators. */
private _isSeparatorKey(event: KeyboardEvent) {
return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.key);
Expand Down
35 changes: 24 additions & 11 deletions projects/novo-elements/src/elements/chips/ChipList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { SelectionModel } from '@angular/cdk/collections';
import {
AfterContentInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Expand Down Expand Up @@ -56,7 +57,7 @@ export class NovoChipListChange {
}

/**
* A material design chips component (named ChipList for its similarity to the List component).
* A chip list component (named ChipList for its similarity to the List component).
*/
@Component({
selector: 'novo-chip-list',
Expand Down Expand Up @@ -92,7 +93,7 @@ export class NovoChipListChange {
})
export class NovoChipList
extends _NovoChipListMixinBase
implements NovoFieldControl<any>, ControlValueAccessor, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState
implements NovoFieldControl<any>, ControlValueAccessor, AfterViewInit, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState
{
/**
* Implemented as part of NovoFieldControl.
Expand Down Expand Up @@ -212,8 +213,8 @@ export class NovoChipList
return this._value;
}
set value(value: any) {
this.writeValue(value);
this._value = value;
this.writeValue(value);
}
protected _value: any;

Expand Down Expand Up @@ -331,7 +332,7 @@ export class NovoChipList

/** Combined stream of all of the child chips' remove change events. */
get chipRemoveChanges(): Observable<NovoChipEvent> {
return merge(...this.chips.map((chip) => chip.destroyed));
return merge(...this.chips.map((chip) => chip.removed));
}

/** Event emitted when the selected chip list value has been changed by the user. */
Expand Down Expand Up @@ -400,9 +401,6 @@ export class NovoChipList

this._resetChips();

// Reset chips selected/deselected status
this._initializeSelection();

// Check to see if we need to update our tab index
this._updateTabIndex();

Expand All @@ -413,6 +411,11 @@ export class NovoChipList
});
}

ngAfterViewInit() {
// Reset chips selected/deselected status
this._initializeSelection();
}

ngOnInit() {
this._selectionModel = new SelectionModel<NovoChipElement>(this.multiple, undefined, false);
this.stateChanges.next();
Expand Down Expand Up @@ -460,9 +463,19 @@ export class NovoChipList
writeValue(value: any): void {
if (this.chips) {
this._setSelectionByValue(value, false);
this.stateChanges.next();
}
}

addValue(value: any): void {
this.value = [...this.value, value];
this._chipInput.clearValue();
}

removeValue(value: any): void {
this.value = this.value.filter((it) => !this.compareWith(it, value));
}

// Implemented as part of ControlValueAccessor.
registerOnChange(fn: (value: any) => void): void {
this._onChange = fn;
Expand Down Expand Up @@ -620,9 +633,8 @@ export class NovoChipList
// Defer setting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
Promise.resolve().then(() => {
if (this.ngControl || this._value) {
this._setSelectionByValue(this.ngControl ? this.ngControl.value : this._value, false);
this.stateChanges.next();
if (this.ngControl) {
this.value = this.ngControl.value;
}
});
}
Expand Down Expand Up @@ -791,13 +803,14 @@ export class NovoChipList
this._chipRemoveSubscription = this.chipRemoveChanges.subscribe((event) => {
const chip = event.chip;
const chipIndex = this.chips.toArray().indexOf(event.chip);

this.removeValue(chip.value);
// In case the chip that will be removed is currently focused, we temporarily store
// the index in order to be able to determine an appropriate sibling chip that will
// receive focus.
if (this._isValidIndex(chipIndex) && chip._hasFocus) {
this._lastDestroyedChipIndex = chipIndex;
}
this.stateChanges.next();
});
}

Expand Down
2 changes: 2 additions & 0 deletions projects/novo-elements/src/elements/chips/ChipTextControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export interface NovoChipTextControl {

/** Focuses the text control. */
focus(options?: FocusOptions): void;

clearValue(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class NovoOptionBase implements FocusableOption, AfterViewChecked, OnDest
if (!this._selected) {
this._selected = true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
// this._emitSelectionChangeEvent();
}
}

Expand All @@ -143,7 +143,7 @@ export class NovoOptionBase implements FocusableOption, AfterViewChecked, OnDest
if (this._selected) {
this._selected = false;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
// this._emitSelectionChangeEvent();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { hasModifierKey } from '@angular/cdk/keycodes';
import {
AfterContentInit,
Expand All @@ -22,9 +22,10 @@ import {
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { Key } from '../../../utils';
import { fromEvent, merge, of, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { fromEvent, interval, merge, of, Subscription } from 'rxjs';
import { debounce, take } from 'rxjs/operators';
import { BooleanInput, Key } from '../../../utils';
import type { NovoChipList } from '../../chips';
import {
CanDisable,
CanDisableCtor,
Expand Down Expand Up @@ -111,7 +112,7 @@ export class NovoAutocompleteElement
/** Whether the user should be allowed to select multiple options. */
@Input()
get multiple(): boolean {
return this._multiple || !!this._formField._control?.multiple || this._formField._control?.controlType === 'chip-list';
return this._multiple;
}
set multiple(value: boolean) {
this._multiple = coerceBooleanProperty(value);
Expand All @@ -120,6 +121,7 @@ export class NovoAutocompleteElement

/** Whether the toggle button is disabled. */
@Input()
@BooleanInput()
get disabled(): boolean {
if (this._disabled === undefined && this._formField?._control) {
return this._formField._control.disabled;
Expand Down Expand Up @@ -170,6 +172,9 @@ export class NovoAutocompleteElement
this.options.changes.subscribe(() => {
this._watchStateChanges();
this._watchSelectionEvents();
Promise.resolve().then(() => {
this.checkSelectedOptions();
});
});
}

Expand All @@ -180,32 +185,49 @@ export class NovoAutocompleteElement

checkPanel() {
const isTriggered = this.triggerOn(this._formField._control);
console.log('checking', isTriggered);
if (isTriggered && this.element) {
this.openPanel();
}
}

private _setTriggerValue(value: any): void {
const toDisplay = this.displayWith ? this.displayWith(value) : value;
private _setTriggerValue(option: NovoOption): void {
const toDisplay = this.displayWith ? this.displayWith(option) : option?.viewValue;
// Simply falling back to an empty string if the display value is falsy does not work properly.
// The display value can also be the number zero and shouldn't fall back to an empty string.
const inputValue = toDisplay != null ? toDisplay : '';
const displayValue = toDisplay != null ? toDisplay : '';
const optionValue = option.value;
console.log('optionValue', optionValue);
// If it's used within a `NovoField`, we should set it through the property so it can go
// through change detection.
if (this._formField) {
const { controlType, lastCaretPosition = 0 } = this._formField._control;
if (controlType === 'textarea') {
const currentValue = this._formField._control.value.split('');
currentValue.splice(lastCaretPosition, 0, inputValue);
currentValue.splice(lastCaretPosition, 0, displayValue);
this._formField._control.value = currentValue.join('');
} else if (controlType === 'chip-list') {
const chipList = this._formField._control as NovoChipList;
const currentValue = this._formField._control.value;
if (currentValue.includes(optionValue)) {
chipList.removeValue(optionValue);
} else {
chipList.addValue(optionValue);
}
} else {
let valueToEmit: any = inputValue;
let valueToEmit: any = optionValue;
if (this.multiple) {
const currentValue = this._formField._control.value;
if (Array.isArray(currentValue)) {
valueToEmit = [...currentValue, inputValue];
if (currentValue.includes(optionValue)) {
valueToEmit = currentValue.filter((it) => it === optionValue);
} else {
valueToEmit = [...currentValue, optionValue];
}
} else if (currentValue === optionValue) {
valueToEmit = [];
} else {
valueToEmit = [currentValue, inputValue];
valueToEmit = [currentValue, optionValue];
}
}
this._formField._control.value = valueToEmit;
Expand All @@ -214,7 +236,7 @@ export class NovoAutocompleteElement
// this._element.nativeElement.value = inputValue;
console.warn(`AutoComplete only intended to be used within a NovoField`);
}
this._previousValue = inputValue;
this._previousValue = optionValue;
}

/**
Expand All @@ -241,16 +263,16 @@ export class NovoAutocompleteElement
*/
private _setValueAndClose(event: NovoOptionSelectionChange | null): void {
if (event && event.source) {
this._clearPreviousSelectedOption(event.source);
this._setTriggerValue(event.source.value);
if (!this.multiple) this._clearPreviousSelectedOption(event.source);
this._setTriggerValue(event.source);
// this._onChange(event.source.value);
// this._element.nativeElement.focus();
this._formField._control.focus();
// this._formField._control.focus();
this._emitSelectEvent(event.source);
this._watchSelectionEvents();
}

if (!this._multiple) this.closePanel();
if (!this.multiple) this.closePanel();
}

private _watchSelectionEvents() {
Expand All @@ -262,12 +284,16 @@ export class NovoAutocompleteElement
}

private _watchStateChanges() {
const inputStateChanged = this._formField && this._formField._control ? this._formField._control.stateChanges : of();
const inputStateChanged = this._formField.stateChanges;
this._stateChanges.unsubscribe();
this._stateChanges = merge(inputStateChanged).subscribe(() => {
this.checkPanel();
this.cdr.markForCheck();
});
this._stateChanges = merge(inputStateChanged)
.pipe(debounce(() => interval(10)))
.subscribe(() => {
console.log('StateChagn');
this.checkSelectedOptions();
this.checkPanel();
this.cdr.markForCheck();
});
}

/** The currently active option, coerced to MatOption type. */
Expand Down Expand Up @@ -310,5 +336,22 @@ export class NovoAutocompleteElement
}
}

static ngAcceptInputType_disabled: BooleanInput;
private checkSelectedOptions() {
if (this.multiple && Array.isArray(this._formField._control.value)) {
const value = this._formField._control.value;
this.options.forEach((option) => option.deselect());
value.forEach((currentValue: any) => this._selectValue(currentValue));
}
}
/**
* Finds and selects and option based on its value.
* @returns Option that has the corresponding value.
*/
private _selectValue(value: any): NovoOption | undefined {
const correspondingOption = this.options.find((option: NovoOption) => {
return option.value === value;
});
correspondingOption?.select();
return correspondingOption;
}
}
Loading

0 comments on commit 94454c6

Please sign in to comment.