diff --git a/projects/novo-elements/src/elements/chips/ChipInput.ts b/projects/novo-elements/src/elements/chips/ChipInput.ts index 404e2ca1c..9fa7c98db 100644 --- a/projects/novo-elements/src/elements/chips/ChipInput.ts +++ b/projects/novo-elements/src/elements/chips/ChipInput.ts @@ -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'; @@ -19,8 +20,8 @@ export interface NovoChipInputEvent { let nextUniqueId = 0; /** - * Directive that adds chip-specific behaviors to an input element inside ``. - * May be placed inside or outside of an ``. + * Directive that adds chip-specific behaviors to an input element inside ``. + * May be placed inside or outside of an ``. */ @Directive({ selector: 'input[novoChipInput]', @@ -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() @@ -94,6 +95,7 @@ export class NovoChipInput implements NovoChipTextControl, OnChanges { protected _elementRef: ElementRef, @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); @@ -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); diff --git a/projects/novo-elements/src/elements/chips/ChipList.ts b/projects/novo-elements/src/elements/chips/ChipList.ts index 5c6f8741b..ebee9532c 100644 --- a/projects/novo-elements/src/elements/chips/ChipList.ts +++ b/projects/novo-elements/src/elements/chips/ChipList.ts @@ -4,6 +4,7 @@ import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; import { SelectionModel } from '@angular/cdk/collections'; import { AfterContentInit, + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -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', @@ -92,7 +93,7 @@ export class NovoChipListChange { }) export class NovoChipList extends _NovoChipListMixinBase - implements NovoFieldControl, ControlValueAccessor, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState + implements NovoFieldControl, ControlValueAccessor, AfterViewInit, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState { /** * Implemented as part of NovoFieldControl. @@ -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; @@ -331,7 +332,7 @@ export class NovoChipList /** Combined stream of all of the child chips' remove change events. */ get chipRemoveChanges(): Observable { - 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. */ @@ -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(); @@ -413,6 +411,11 @@ export class NovoChipList }); } + ngAfterViewInit() { + // Reset chips selected/deselected status + this._initializeSelection(); + } + ngOnInit() { this._selectionModel = new SelectionModel(this.multiple, undefined, false); this.stateChanges.next(); @@ -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; @@ -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; } }); } @@ -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(); }); } diff --git a/projects/novo-elements/src/elements/chips/ChipTextControl.ts b/projects/novo-elements/src/elements/chips/ChipTextControl.ts index 248db8693..bc3832c8b 100644 --- a/projects/novo-elements/src/elements/chips/ChipTextControl.ts +++ b/projects/novo-elements/src/elements/chips/ChipTextControl.ts @@ -14,4 +14,6 @@ export interface NovoChipTextControl { /** Focuses the text control. */ focus(options?: FocusOptions): void; + + clearValue(): void; } diff --git a/projects/novo-elements/src/elements/common/option/option.component.ts b/projects/novo-elements/src/elements/common/option/option.component.ts index 919be9176..507dee496 100644 --- a/projects/novo-elements/src/elements/common/option/option.component.ts +++ b/projects/novo-elements/src/elements/common/option/option.component.ts @@ -134,7 +134,7 @@ export class NovoOptionBase implements FocusableOption, AfterViewChecked, OnDest if (!this._selected) { this._selected = true; this._changeDetectorRef.markForCheck(); - this._emitSelectionChangeEvent(); + // this._emitSelectionChangeEvent(); } } @@ -143,7 +143,7 @@ export class NovoOptionBase implements FocusableOption, AfterViewChecked, OnDest if (this._selected) { this._selected = false; this._changeDetectorRef.markForCheck(); - this._emitSelectionChangeEvent(); + // this._emitSelectionChangeEvent(); } } diff --git a/projects/novo-elements/src/elements/field/autocomplete/autocomplete.component.ts b/projects/novo-elements/src/elements/field/autocomplete/autocomplete.component.ts index 9001a52ac..4661daf11 100644 --- a/projects/novo-elements/src/elements/field/autocomplete/autocomplete.component.ts +++ b/projects/novo-elements/src/elements/field/autocomplete/autocomplete.component.ts @@ -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, @@ -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, @@ -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); @@ -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; @@ -170,6 +172,9 @@ export class NovoAutocompleteElement this.options.changes.subscribe(() => { this._watchStateChanges(); this._watchSelectionEvents(); + Promise.resolve().then(() => { + this.checkSelectedOptions(); + }); }); } @@ -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; @@ -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; } /** @@ -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() { @@ -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. */ @@ -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; + } } diff --git a/projects/novo-elements/src/elements/field/field.ts b/projects/novo-elements/src/elements/field/field.ts index 38dac0e07..cd488c8b0 100644 --- a/projects/novo-elements/src/elements/field/field.ts +++ b/projects/novo-elements/src/elements/field/field.ts @@ -8,9 +8,11 @@ import { ContentChildren, Directive, ElementRef, + EventEmitter, InjectionToken, Input, OnDestroy, + Output, QueryList, ViewChild, } from '@angular/core'; @@ -99,6 +101,9 @@ export class NovoFieldElement implements AfterContentInit, OnDestroy { private _destroyed = new Subject(); + @Output() valueChanges: EventEmitter = new EventEmitter(); + @Output() stateChanges: EventEmitter = new EventEmitter(); + constructor(public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef) {} /** * Gets an ElementRef for the element that a overlay attached to the form-field should be @@ -120,12 +125,16 @@ export class NovoFieldElement implements AfterContentInit, OnDestroy { // Subscribe to changes in the child control state in order to update the form field UI. // tslint:disable-next-line:deprecation control.stateChanges.pipe(startWith(null)).subscribe(() => { + this.stateChanges.next(); this._changeDetectorRef.markForCheck(); }); // Run change detection if the value changes. if (control.ngControl && control.ngControl.valueChanges) { - control.ngControl.valueChanges.pipe(takeUntil(this._destroyed)).subscribe(() => this._changeDetectorRef.markForCheck()); + control.ngControl.valueChanges.pipe(takeUntil(this._destroyed)).subscribe((v) => { + this.valueChanges.next(v); + this._changeDetectorRef.markForCheck(); + }); } if (this._hasLabel()) { diff --git a/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.html b/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.html index 9033783f5..410f1fa41 100644 --- a/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.html +++ b/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.html @@ -1,8 +1,13 @@ Shifts - + @@ -28,7 +33,7 @@ [formControl]="searchCtrl" (novoChipInputTokenEnd)="add($event)" /> - + calendar diff --git a/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.ts b/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.ts index 51f5fd32a..d680058d7 100644 --- a/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.ts +++ b/projects/novo-examples/src/components/autocomplete/autocomplete-stacked-chips/autocomplete-stacked-chips-example.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl } from '@angular/forms'; import { NovoOptionSelectedEvent } from 'novo-elements'; // import { NovoChipInputEvent } from 'novo-elements'; @@ -22,12 +22,10 @@ interface ShiftData { styleUrls: ['autocomplete-stacked-chips-example.css'], }) export class AutocompleteStackedChipsExample { - searchCtrl = new FormControl(); filteredShifts: Observable; - shifts: ShiftData[] = ALL_SHIFTS.slice(0, 3); allShifts: ShiftData[] = ALL_SHIFTS; - - @ViewChild('searchInput') searchInput: ElementRef; + searchCtrl = new FormControl(); + shiftCtrl = new FormControl(ALL_SHIFTS.slice(0, 3)); constructor() { this.filteredShifts = this.searchCtrl.valueChanges.pipe( @@ -36,40 +34,18 @@ export class AutocompleteStackedChipsExample { ); } - add(event: any): void { - const input = event.input; - const value = event.value; + add(event: any): void {} - // Add our shift - if ((value || '').trim()) { - this.shifts.push(value.trim()); - } + remove(shift: ShiftData): void {} - // Reset the input value - if (input) { - input.value = ''; - } + selected(event: NovoOptionSelectedEvent): void {} - this.searchCtrl.setValue(null); - } - - remove(shift: ShiftData): void { - const index = this.shifts.indexOf(shift); - - if (index >= 0) { - this.shifts.splice(index, 1); - } - } - - selected(event: NovoOptionSelectedEvent): void { - this.shifts.push(event.option.value); - this.searchInput.nativeElement.value = ''; - this.searchCtrl.setValue(null); + compareById(o1: any, o2: any) { + return o1.id === o2.id; } private _filter(value: string): ShiftData[] { const filterValue = value.toLowerCase(); - return this.allShifts.filter((shift) => shift.startTime.toLowerCase().indexOf(filterValue) === 0); } } diff --git a/projects/novo-examples/src/components/autocomplete/autocomplete-textarea/autocomplete-textarea-example.html b/projects/novo-examples/src/components/autocomplete/autocomplete-textarea/autocomplete-textarea-example.html index eb9871a0c..7e48dd4c1 100644 --- a/projects/novo-examples/src/components/autocomplete/autocomplete-textarea/autocomplete-textarea-example.html +++ b/projects/novo-examples/src/components/autocomplete/autocomplete-textarea/autocomplete-textarea-example.html @@ -16,7 +16,7 @@ Toppings - {{option}} + {{option}} \ No newline at end of file diff --git a/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.html b/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.html index df7c44449..faac7c72b 100644 --- a/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.html +++ b/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.html @@ -1,19 +1,20 @@ Favorite Fruits - + {{fruit}} close @@ -23,5 +24,5 @@ -
Chip List Value: {{chipList.value}}
-
Form Control Value: {{fruitCtrl.value}}
\ No newline at end of file +
Chip List Value: {{fieldCtrl.value}}
+
Search Input Control Value: {{searchCtrl.value}}
\ No newline at end of file diff --git a/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.ts b/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.ts index 024352c7d..c75ca6779 100644 --- a/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.ts +++ b/projects/novo-examples/src/components/autocomplete/autocomplete-with-chips/autocomplete-with-chips-example.ts @@ -19,50 +19,25 @@ export class AutocompleteWithChipsExample { selectable = true; removable = true; separatorKeysCodes: number[] = [ENTER, COMMA]; - fruitCtrl = new FormControl(); + searchCtrl = new FormControl(); + fieldCtrl = new FormControl(['Lemon']); filteredFruits: Observable; - fruits: string[] = ['Lemon']; allFruits: string[] = ['Apple', 'Lemon', 'Lime', 'Orange', 'Strawberry']; - @ViewChild('fruitInput') fruitInput: ElementRef; + @ViewChild('chipInput') chipInput: ElementRef; constructor() { - this.filteredFruits = this.fruitCtrl.valueChanges.pipe( + this.filteredFruits = this.searchCtrl.valueChanges.pipe( startWith(null), map((fruit: string | null) => (fruit ? this._filter(fruit) : this.allFruits.slice())), ); } - add(event: any): void { - const input = event.input; - const value = event.value; + add(event: any): void {} - // Add our fruit - if ((value || '').trim()) { - this.fruits.push(value.trim()); - } + remove(fruit: string): void {} - // Reset the input value - if (input) { - input.value = ''; - } - - this.fruitCtrl.setValue(null); - } - - remove(fruit: string): void { - const index = this.fruits.indexOf(fruit); - - if (index >= 0) { - this.fruits.splice(index, 1); - } - } - - selected(event: NovoOptionSelectedEvent): void { - this.fruits.push(event.option.viewValue); - this.fruitInput.nativeElement.value = ''; - this.fruitCtrl.setValue(null); - } + selected(event: NovoOptionSelectedEvent): void {} private _filter(value: string): string[] { const filterValue = value.toLowerCase();