diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.html b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.html new file mode 100644 index 00000000..454fd836 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.html @@ -0,0 +1,12 @@ +
+ + Fruit + + + +
diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.scss b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.scss new file mode 100644 index 00000000..10c18d25 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.scss @@ -0,0 +1,5 @@ +.es-chips-autocomplete-story-basic { + &.mat-form-field { + width: 100%; + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.ts new file mode 100644 index 00000000..01e9cf9d --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.component.ts @@ -0,0 +1,28 @@ +import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { GetFilterOptions } from '../../filter-options'; + +const OPTIONS = ['Apple', 'Lemon', 'Mango']; + +@Component({ + selector: 'es-chips-autocomplete-story-basic', + templateUrl: './chips-autocomplete-story-basic.component.html', + styleUrls: ['./chips-autocomplete-story-basic.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class ChipsAutocompleteBasicComponent { + public form: FormGroup; + public options: string[] = OPTIONS; + public color = 'accent'; + + constructor(private formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + chips: [] + }); + } + + public onChangeText(text: string) { + this.options = GetFilterOptions(text, OPTIONS); + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.module.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.module.ts new file mode 100644 index 00000000..c06243cb --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { MatFormFieldModule } from '@angular/material/form-field'; + +import { ES_CHIPS_DEFAULT_OPTIONS } from '../../chips-autocomplete.component'; +import { ESChipsAutocompleteModule } from '../../chips-autocomplete.module'; +import { ChipsAutocompleteBasicComponent } from './chips-autocomplete-story-basic.component'; + +@NgModule({ + declarations: [ChipsAutocompleteBasicComponent], + imports: [CommonModule, ReactiveFormsModule, MatFormFieldModule, ESChipsAutocompleteModule], + exports: [ChipsAutocompleteBasicComponent], + providers: [ + { + provide: ES_CHIPS_DEFAULT_OPTIONS, + useValue: { + debounceTime: 1000, + freeInput: true, + unique: true, + selectable: true, + removable: true + } + } + ] +}) +export class ESChipsAutocompleteBasicModule {} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.source.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.source.ts new file mode 100644 index 00000000..0b57a478 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.source.ts @@ -0,0 +1,36 @@ +export const CHIPS_AUTOCOMPLETE_STORY_BASIC_SOURCE = { + html: ` +
+ + Fruit + + + +
`, + ts: ` + import { GetFilterOptions } from '@elonsoft/elonkit/chips-autocomplete'; + const OPTIONS = ['Apple', 'Lemon', 'Mango']; + + @Component(...) + export class ChipsAutocompleteBasicComponent { + public form: FormGroup; + public options = OPTIONS; + public color = 'accent'; + + constructor(private formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + chips: [] + }); + } + + public onChangeText(text: string) { + this.options = GetFilterOptions(text, OPTIONS); + } + } + ` +}; diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/index.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/index.ts new file mode 100644 index 00000000..bc1fac50 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-basic/index.ts @@ -0,0 +1 @@ +export { CHIPS_AUTOCOMPLETE_STORY_BASIC_SOURCE } from './chips-autocomplete-story-basic.source'; diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.html b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.html new file mode 100644 index 00000000..07eccbee --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.html @@ -0,0 +1,12 @@ +
+ + Countries + + + +
diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.scss b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.scss new file mode 100644 index 00000000..0588f18b --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.scss @@ -0,0 +1,5 @@ +.es-chips-autocomplete-story-checkbox { + &.mat-form-field { + width: 100%; + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.ts new file mode 100644 index 00000000..48c98a15 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.component.ts @@ -0,0 +1,28 @@ +import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { GetFilterOptions } from '../../filter-options'; + +const OPTIONS = ['Russia', 'Spain', 'India']; + +@Component({ + selector: 'es-chips-autocomplete-story-checkbox', + templateUrl: './chips-autocomplete-story-checkbox.component.html', + styleUrls: ['./chips-autocomplete-story-checkbox.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class ChipsAutocompleteCheckboxComponent { + public form: FormGroup; + public options: string[] = OPTIONS; + public withCheckbox = true; + + constructor(private formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + chips: [] + }); + } + + public onChangeText(text: string) { + this.options = GetFilterOptions(text, OPTIONS); + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.module.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.module.ts new file mode 100644 index 00000000..c934fe10 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { MatFormFieldModule } from '@angular/material/form-field'; + +import { ES_CHIPS_DEFAULT_OPTIONS } from '../../chips-autocomplete.component'; +import { ESChipsAutocompleteModule } from '../../chips-autocomplete.module'; +import { ChipsAutocompleteCheckboxComponent } from './chips-autocomplete-story-checkbox.component'; + +@NgModule({ + declarations: [ChipsAutocompleteCheckboxComponent], + imports: [CommonModule, ReactiveFormsModule, MatFormFieldModule, ESChipsAutocompleteModule], + exports: [ChipsAutocompleteCheckboxComponent], + providers: [ + { + provide: ES_CHIPS_DEFAULT_OPTIONS, + useValue: { + debounceTime: 1000, + freeInput: false, + unique: false, + selectable: true, + removable: true + } + } + ] +}) +export class ESChipsAutocompleteCheckboxModule {} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.source.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.source.ts new file mode 100644 index 00000000..d361bb98 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.source.ts @@ -0,0 +1,36 @@ +export const CHIPS_AUTOCOMPLETE_STORY_CHECKBOX_SOURCE = { + html: ` +
+ + Countries + + + +
`, + ts: ` + import { GetFilterOptions } from '@elonsoft/elonkit/chips-autocomplete'; + const OPTIONS = ['Russia', 'Spain', 'India']; + + @Component(...) + export class ChipsAutocompleteCheckboxComponent { + public form: FormGroup; + public options = OPTIONS; + public withCheckbox = true; + + constructor(private formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + chips: [] + }); + } + + public onChangeText(text: string) { + this.options = GetFilterOptions(text, OPTIONS); + } + } + ` +}; diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/index.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/index.ts new file mode 100644 index 00000000..cf3b14c3 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-checkbox/index.ts @@ -0,0 +1 @@ +export { CHIPS_AUTOCOMPLETE_STORY_CHECKBOX_SOURCE } from './chips-autocomplete-story-checkbox.source'; diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.html b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.html new file mode 100644 index 00000000..65394a89 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.html @@ -0,0 +1,21 @@ +
+ + Friends + + + + {{ option.name }} + + + + {{ option.name }} + + + +
diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.scss b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.scss new file mode 100644 index 00000000..cd07f6cb --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.scss @@ -0,0 +1,11 @@ +.es-chips-autocomplete-story-custom { + &__option-img { + height: 25px; + margin-right: 8px; + vertical-align: middle; + } + + &.mat-form-field { + width: 100%; + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.ts new file mode 100644 index 00000000..bb2b5fab --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.component.ts @@ -0,0 +1,48 @@ +import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { GetFilterOptionsByKey } from '../../filter-options'; + +const OPTIONS = [ + { + name: 'Anna', + photo: 'https://joeschmoe.io/api/v1/jenni' + }, + { + name: 'Mary', + photo: 'https://joeschmoe.io/api/v1/julie' + }, + { + name: 'Elena', + photo: 'https://joeschmoe.io/api/v1/jolee' + } +]; + +@Component({ + selector: 'es-chips-autocomplete-story-custom', + templateUrl: './chips-autocomplete-story-custom.component.html', + styleUrls: ['./chips-autocomplete-story-custom.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class ChipsAutocompleteCustomComponent { + public form: FormGroup; + public options: any[] = OPTIONS; + + constructor(private formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + chips: [] + }); + } + + public onChangeText(text: string) { + this.options = GetFilterOptionsByKey(text, OPTIONS, 'name'); + } + + public valueFn(option: any): any { + return option; + } + + public displayWith(option: any): any { + return option.name; + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.module.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.module.ts new file mode 100644 index 00000000..2edcf3e0 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { MatFormFieldModule } from '@angular/material/form-field'; + +import { ES_CHIPS_DEFAULT_OPTIONS } from '../../chips-autocomplete.component'; +import { ESChipsAutocompleteModule } from '../../chips-autocomplete.module'; +import { ChipsAutocompleteCustomComponent } from './chips-autocomplete-story-custom.component'; + +@NgModule({ + declarations: [ChipsAutocompleteCustomComponent], + imports: [CommonModule, ReactiveFormsModule, MatFormFieldModule, ESChipsAutocompleteModule], + exports: [ChipsAutocompleteCustomComponent], + providers: [ + { + provide: ES_CHIPS_DEFAULT_OPTIONS, + useValue: { + debounceTime: 1000, + freeInput: false, + unique: true, + selectable: true, + removable: true + } + } + ] +}) +export class ESChipsAutocompleteCustomModule {} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.source.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.source.ts new file mode 100644 index 00000000..1adbd5b9 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.source.ts @@ -0,0 +1,73 @@ +export const CHIPS_AUTOCOMPLETE_STORY_CUSTOM_SOURCE = { + html: ` +
+ + Friends + + + + {{ option.name }} + + + + {{ option.name }} + + + +
`, + ts: ` + import { GetFilterOptionsByKey } from '@elonsoft/elonkit/chips-autocomplete'; + const OPTIONS = [ + { + name: 'Anna', + photo: 'https://joeschmoe.io/api/v1/jenni' + }, + { + name: 'Mary', + photo: 'https://joeschmoe.io/api/v1/julie' + }, + { + name: 'Elena', + photo: 'https://joeschmoe.io/api/v1/jolee' + } + ]; + + @Component(...) + export class ChipsAutocompleteCustomComponent { + public form: FormGroup; + public options = OPTIONS; + + constructor(private formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + chips: [] + }); + } + public onChangeText(text: string) { + this.options = GetFilterOptionsByKey(text, OPTIONS, 'name'); + } + + public valueFn(option: any): any { + return option; + } + + public displayWith(option: any): any { + return option.name; + } + } + `, + scss: ` + .es-chips-autocomplete-story-custom { + &__option-img { + height: 25px; + margin-right: 8px; + vertical-align: middle; + } + } + ` +}; diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/index.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/index.ts new file mode 100644 index 00000000..8a718f82 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/__stories__/chips-autocomplete-story-custom/index.ts @@ -0,0 +1 @@ +export { CHIPS_AUTOCOMPLETE_STORY_CUSTOM_SOURCE } from './chips-autocomplete-story-custom.source'; diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chip.directive.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/chip.directive.ts new file mode 100644 index 00000000..66d76606 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chip.directive.ts @@ -0,0 +1,6 @@ +import { Directive } from '@angular/core'; + +@Directive({ + selector: '[esChip]' +}) +export class ChipDirective {} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.html b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.html new file mode 100644 index 00000000..abf27891 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.html @@ -0,0 +1,57 @@ + + + {{ displayWith(chip) }} + + + + cancel + + + + + + + + +
+ + {{ option }} + + + +
+
+
+
diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.scss b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.scss new file mode 100644 index 00000000..b2a08072 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.scss @@ -0,0 +1,14 @@ +.es-chips-autocomplete { + &__option-checkbox { + &.mat-checkbox { + margin-left: 12px; + margin-right: 15px; + } + } +} + +input { + flex: 1 0 150px; + margin: 4px; + width: 150px; +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.ts new file mode 100644 index 00000000..c8db6ed8 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.component.ts @@ -0,0 +1,572 @@ +import { + Component, + ChangeDetectionStrategy, + ViewEncapsulation, + OnDestroy, + OnInit, + Input, + HostBinding, + Optional, + Self, + ChangeDetectorRef, + Output, + EventEmitter, + ViewChild, + ContentChild, + TemplateRef, + InjectionToken, + Inject, + Host +} from '@angular/core'; + +import { ControlValueAccessor, NgControl, FormGroupDirective } from '@angular/forms'; + +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +import { MatFormFieldControl, MatFormField } from '@angular/material/form-field'; +import { + MatAutocompleteTrigger, + MatAutocomplete, + MatAutocompleteOrigin +} from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; + +import { Subject, timer } from 'rxjs'; +import { debounce } from 'rxjs/operators'; + +import { ChipsAutocompleteOptionDirective } from '../chips-autocomplete/chips-autocomplete.directive'; +import { ChipDirective } from '../chips-autocomplete/chip.directive'; + +import { ENTER, COMMA, SEMICOLON } from '@angular/cdk/keycodes'; + +export const ES_CHIPS_DEFAULT_OPTIONS = new InjectionToken( + 'ES_CHIPS_DEFAULT_OPTIONS' +); + +export interface EsAutocompleteDefaultOptions { + debounceTime?: number; + freeInput?: boolean; + unique?: boolean; + selectable?: boolean; + removable?: boolean; +} + +@Component({ + selector: 'es-chips-autocomplete', + templateUrl: './chips-autocomplete.component.html', + styleUrls: ['./chips-autocomplete.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + providers: [{ provide: MatFormFieldControl, useExisting: ChipsAutocompleteComponent }] +}) +export class ChipsAutocompleteComponent + implements MatFormFieldControl, ControlValueAccessor, OnDestroy, OnInit { + /** + * The list of key codes that will trigger a chipEnd event + */ + public separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + private static nextId = 0; + /** + * @ignore + */ + public text = ''; + /** + * @ignore + */ + public stateChanges = new Subject(); + private text$ = new Subject(); + + public get value(): any { + return this._value; + } + + public set value(value: any) { + this._value = value; + this.stateChanges.next(); + } + + public get focused() { + return this._focused; + } + public set focused(focused: boolean) { + this._focused = focused; + this.stateChanges.next(); + } + + public get empty(): boolean { + return !this.value; + } + + /** + * @ignore + */ + public origin: MatAutocompleteOrigin; + + // tslint:disable-next-line variable-name + private _debounceTime: number; + + // tslint:disable-next-line variable-name + private _freeInput: boolean; + + // tslint:disable-next-line variable-name + private _unique: boolean; + + // tslint:disable-next-line variable-name + private _selectable: boolean; + + // tslint:disable-next-line variable-name + private _removable: boolean; + + // tslint:disable-next-line variable-name + private _value: T[] = []; + + // tslint:disable-next-line variable-name + private _focused = false; + + // tslint:disable-next-line variable-name + private _required = false; + + // tslint:disable-next-line variable-name + private _disabled = false; + + // tslint:disable-next-line variable-name + private _placeholder = ''; + + /** + * Array of options + */ + @Input() public options: T[]; + + /** + * Color of chips + */ + @Input() public color: string; + + /** + * If true the user have options with checkbox + */ + @Input() public withCheckbox: false; + + /** + * Change value after a particular time span has passed + */ + @Input() + public get debounceTime(): number { + return this._debounceTime; + } + public set debounceTime(value: number) { + this._debounceTime = + value || + (this.autocompleteDefaultOptions && this.autocompleteDefaultOptions.debounceTime) || + 0; + } + + /** + * If true the user input is not bound to provided options + */ + @Input() + public get freeInput(): boolean { + return this._freeInput; + } + public set freeInput(value: boolean) { + this._freeInput = + value !== undefined + ? value + : this.autocompleteDefaultOptions && this.autocompleteDefaultOptions.freeInput + ? this.autocompleteDefaultOptions.freeInput + : false; + } + + /** + * If true the user can choose only unique option + */ + @Input() + public get unique(): boolean { + return this._unique; + } + public set unique(value: boolean) { + this._unique = + value !== undefined + ? value + : this.autocompleteDefaultOptions && this.autocompleteDefaultOptions.unique + ? this.autocompleteDefaultOptions.unique + : false; + } + + /** + * If true this chips list is selectable + */ + @Input() + public get selectable(): boolean { + return this._selectable; + } + public set selectable(value: boolean) { + this._selectable = + value !== undefined + ? value + : this.autocompleteDefaultOptions && this.autocompleteDefaultOptions.selectable + ? this.autocompleteDefaultOptions.selectable + : false; + } + + /** + * If true this chips list is removable + */ + @Input() + public get removable(): boolean { + return this._removable; + } + public set removable(value: boolean) { + this._removable = + value !== undefined + ? value + : this.autocompleteDefaultOptions && this.autocompleteDefaultOptions.removable + ? this.autocompleteDefaultOptions.removable + : false; + } + + /** + * This property is used to indicate whether the input is required + */ + @Input() + public get required() { + return this._required; + } + public set required(req) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + + /** + * This property tells the form field when it should be in the disabled state + */ + @Input() + public get disabled(): boolean { + return this._disabled; + } + public set disabled(dis: boolean) { + this._disabled = coerceBooleanProperty(dis); + this.stateChanges.next(); + } + + /** + * This property allows us to tell component what to use as a placeholder + */ + @Input() + public get placeholder(): string { + return this._placeholder; + } + + public set placeholder(plh) { + this._placeholder = plh; + this.stateChanges.next(); + } + + /** + * @ignore + */ + @Input() public isLoading = false; + + /** + * Function that maps an option control value to its display value in the trigger + */ + @Input() public displayWith = (option: T): string => { + return '' + option; + }; + + /** + * Function that have chosen value + */ + @Input() public valueFn = (option: any): any => { + return option; + }; + + /** + * Function that have compared values + */ + @Input() public compareWith = (a: T, b: T) => a === b; + + /** + * @ignore + */ + public isOptionVisible(option: T): boolean { + if (this.unique) { + if (this.value) { + return !this.value.some(e => this.compareWith(e, option)); + } + } else { + return true; + } + } + + /** + * Event emitted when user change text in input + */ + @Output() public changeText = new EventEmitter(); + + @ViewChild('inputChild', { read: MatAutocompleteTrigger, static: true }) + private inputChild: MatAutocompleteTrigger; + @ViewChild('auto', { static: false }) private matAutocomplete: MatAutocomplete; + + /** + * Template that allows add custom options + */ + @ContentChild(ChipsAutocompleteOptionDirective, { read: TemplateRef, static: false }) + public optionTemplate: any; + + /** + * Template that allows add custom chips + */ + @ContentChild(ChipDirective, { read: TemplateRef, static: false }) + public chipTemplate: any; + + @HostBinding() public id = `es-autocomplete-${ChipsAutocompleteComponent.nextId++}`; + @HostBinding('attr.aria-describedby') public describedBy = ''; + + public get errorState(): boolean { + const control = this.ngControl; + const form = this.ngForm; + + if (control) { + return control.invalid && (control.touched || (form && form.submitted)); + } + + return false; + } + + @HostBinding('class.floating') + public get shouldLabelFloat(): boolean { + return this.focused || !!this.text || (this.value && this.value.length > 0); + } + + /** + * @ignore + */ + constructor( + public changeDetector: ChangeDetectorRef, + @Optional() @Self() public ngControl: NgControl, + @Optional() + public ngForm: FormGroupDirective, + @Optional() + @Inject(ES_CHIPS_DEFAULT_OPTIONS) + private autocompleteDefaultOptions: EsAutocompleteDefaultOptions, + @Optional() @Host() private matFormField: MatFormField + ) { + if (this.ngControl != null) { + this.ngControl.valueAccessor = this; + } + this.debounceTime = + autocompleteDefaultOptions && autocompleteDefaultOptions.debounceTime + ? autocompleteDefaultOptions.debounceTime + : 0; + this.freeInput = + autocompleteDefaultOptions && autocompleteDefaultOptions.freeInput + ? autocompleteDefaultOptions.freeInput + : false; + + this.unique = + autocompleteDefaultOptions && autocompleteDefaultOptions.unique + ? autocompleteDefaultOptions.unique + : false; + + this.selectable = + autocompleteDefaultOptions && autocompleteDefaultOptions.selectable + ? autocompleteDefaultOptions.selectable + : false; + + this.removable = + autocompleteDefaultOptions && autocompleteDefaultOptions.removable + ? autocompleteDefaultOptions.removable + : false; + + this.stateChanges.subscribe(() => { + this.changeDetector.detectChanges(); + }); + } + + /** + * @ignore + */ + public ngOnInit() { + this.text$.pipe(debounce(() => timer(this.debounceTime))).subscribe(text => { + this.changeText.emit(text); + }); + if (this.matFormField) { + this.origin = { + elementRef: this.matFormField.getConnectedOverlayOrigin() + }; + } + } + + /** + * @ignore + */ + public ngOnDestroy() { + this.stateChanges.complete(); + this.changeText.complete(); + } + + /** + * @ignore + */ + public setDescribedByIds(ids: string[]) { + this.describedBy = ids.join(' '); + } + + /** + * @ignore + */ + public onContainerClick(event: MouseEvent) { + this.openPanel(event); + } + + /** + * @ignore + */ + private openPanel(event: MouseEvent) { + if (!this.focused && !this.disabled && this.inputChild) { + event.stopPropagation(); + this.inputChild.openPanel(); + (this.inputChild as any)._element.nativeElement.focus(); + this.stateChanges.next(); + } + } + + /** + * @ignore + */ + public writeValue(value: any) { + if (!!value) { + this.value = value; + this.text = this.value; + this.stateChanges.next(); + } + } + + /** + * @ignore + */ + public registerOnChange(onChange: (value: any) => void) { + this.onChange = onChange; + } + + /** + * @ignore + */ + public onChange = (_: any) => {}; + + /** + * @ignore + */ + public registerOnTouched(onTouched: () => void) { + this.onTouched = onTouched; + } + + /** + * @ignore + */ + public onTouched = () => {}; + + /** + * @ignore + */ + public onInput(event: Event) { + const target = event.target as HTMLInputElement; + this.text = target.value; + this.text$.next(this.text); + this.onChange(this.text); + this.stateChanges.next(); + } + + /** + * @ignore + */ + public onFocus() { + this.focused = true; + this.stateChanges.next(); + } + + /** + * @ignore + */ + public onBlur() { + this.onTouched(); + this.focused = false; + this.stateChanges.next(); + } + + /** + * @ignore + */ + public onSuggestionSelect(event: Event) { + this.value = this.value.concat(event); + this.onChange(this.value); + this.stateChanges.next(); + (this.inputChild as any)._element.nativeElement.value = ''; + } + + /** + * @ignore + */ + public onRemove(index: number) { + this.value.splice(index, 1); + this.onChange(this.value); + this.stateChanges.next(); + } + + /** + * @ignore + */ + public add(event: MatChipInputEvent) { + const input = event.input; + const value = event.value; + if (this.freeInput) { + if ((value || '').trim()) { + this.value = this.value.concat(value.trim()); + this.onChange(this.value); + this.stateChanges.next(); + } + } else { + const opt = this.options.find( + option => this.displayWith(option).toLowerCase() === value.toLowerCase() + ); + if (opt && !this.value.find(e => this.displayWith(e).toLowerCase() === value.toLowerCase())) { + this.value = this.value.concat(opt); + this.onChange(this.value); + this.stateChanges.next(); + } + } + if (input) { + input.value = ''; + } + } + + /** + * @ignore + */ + public onSelect(event: MouseEvent, option: T) { + if (this.withCheckbox) { + event.stopPropagation(); + + if (this.value) { + if (!this.value.find(e => this.compareWith(e, option))) { + this.value = this.value.concat(option); + } else { + this.value.splice(this.value.indexOf(option), 1); + } + this.onChange(this.value); + this.stateChanges.next(); + } + } + } + + /** + * @ignore + */ + public isOptionChecked(option: T): boolean { + if (this.value) { + return this.value.find(e => this.compareWith(e, option)); + } + return false; + } +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.directive.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.directive.ts new file mode 100644 index 00000000..dab72192 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.directive.ts @@ -0,0 +1,6 @@ +import { Directive } from '@angular/core'; + +@Directive({ + selector: '[esChipsAutocompleteOption]' +}) +export class ChipsAutocompleteOptionDirective {} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.module.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.module.ts new file mode 100644 index 00000000..6e2c8702 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCheckboxModule } from '@angular/material/checkbox'; + +import { ChipsAutocompleteOptionDirective } from './chips-autocomplete.directive'; +import { ChipDirective } from './chip.directive'; + +import { ChipsAutocompleteComponent } from './chips-autocomplete.component'; + +@NgModule({ + declarations: [ChipsAutocompleteComponent, ChipsAutocompleteOptionDirective, ChipDirective], + imports: [ + CommonModule, + MatAutocompleteModule, + MatInputModule, + MatSelectModule, + MatChipsModule, + MatIconModule, + MatCheckboxModule + ], + exports: [ + ChipsAutocompleteComponent, + ChipsAutocompleteOptionDirective, + ChipDirective, + MatSelectModule + ] +}) +export class ESChipsAutocompleteModule {} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.spec.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.spec.ts new file mode 100644 index 00000000..f07ec696 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.spec.ts @@ -0,0 +1,65 @@ +import { render, RenderResult } from '@testing-library/angular'; + +import { inject, fakeAsync, tick } from '@angular/core/testing'; +import { OverlayContainer } from '@angular/cdk/overlay'; + +import { ESChipsAutocompleteModule } from './chips-autocomplete.module'; +import { ChipsAutocompleteComponent } from './chips-autocomplete.component'; + +const FRUITS = ['Apple', 'Lemon', 'Mango']; + +describe('ChipsAutocomplete', () => { + describe('Base', () => { + let component: RenderResult, ChipsAutocompleteComponent>; + let overlay: OverlayContainer; + let overlayElement: HTMLElement; + + beforeEach(async () => { + component = await render(ChipsAutocompleteComponent, { + imports: [ESChipsAutocompleteModule], + componentProperties: { + options: FRUITS + }, + excludeComponentDeclaration: true + }); + + inject([OverlayContainer], (oc: OverlayContainer) => { + overlay = oc; + overlayElement = oc.getContainerElement(); + })(); + }); + + afterEach(inject([OverlayContainer], (currentOverlay: OverlayContainer) => { + currentOverlay.ngOnDestroy(); + overlay.ngOnDestroy(); + })); + + it('Should display passed options', fakeAsync(async () => { + const input = component.getByTestId('input'); + + component.focusIn(input); + + const options = overlayElement.querySelectorAll('[data-testid="mat-option"]'); + expect(options).toHaveLength(3); + + // all items of FRUITS array contain in overlay + for (const fruit of FRUITS) { + expect(overlayElement.textContent).toContain(fruit); + } + })); + + it('Should display chips after chose option', fakeAsync(async () => { + const TEXT_APPLE = 'Apple'; + + const input = component.getByTestId('input'); + component.focusIn(input); + + const options = overlayElement.querySelectorAll('[data-testid="mat-option"]'); + component.click(options[0]); + + const chips = component.getAllByTestId('mat-chip'); + expect(chips).toHaveLength(1); + expect(component.getByText(TEXT_APPLE)).toBeInTheDocument(); + })); + }); +}); diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.stories.mdx b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.stories.mdx new file mode 100644 index 00000000..0f338526 --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/chips-autocomplete.stories.mdx @@ -0,0 +1,96 @@ +import { Meta, Story, Props } from '@storybook/addon-docs/blocks'; +import { Preview } from '~storybook/components'; + +import { withA11y } from '@storybook/addon-a11y'; +import { action } from '@storybook/addon-actions'; +import { withKnobs, text } from '@storybook/addon-knobs'; + +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { ESChipsAutocompleteModule } from './chips-autocomplete.module'; +import { ChipsAutocompleteComponent } from './chips-autocomplete.component'; + +import { ESChipsAutocompleteBasicModule } from './__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.module'; +import { CHIPS_AUTOCOMPLETE_STORY_BASIC_SOURCE } from './__stories__/chips-autocomplete-story-basic/chips-autocomplete-story-basic.source'; + +import { ESChipsAutocompleteCustomModule } from './__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.module'; +import { CHIPS_AUTOCOMPLETE_STORY_CUSTOM_SOURCE } from './__stories__/chips-autocomplete-story-custom/chips-autocomplete-story-custom.source'; + +import { ESChipsAutocompleteCheckboxModule } from './__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.module'; +import { CHIPS_AUTOCOMPLETE_STORY_CHECKBOX_SOURCE } from './__stories__/chips-autocomplete-story-checkbox/chips-autocomplete-story-checkbox.source'; + + + +# ChipsAutocomplete + +This component demonstrates usage of chips autocomplete. + +## Demos + +This is a basic story: + + + + {{ + template: ``, + moduleMetadata: { + imports: [BrowserAnimationsModule, ESChipsAutocompleteBasicModule] + } + }} + + + +## Demos + +This is a custom story: + + + + {{ + template: ``, + moduleMetadata: { + imports: [BrowserAnimationsModule, ESChipsAutocompleteCustomModule] + } + }} + + + +This is a checkbox story: + + + + {{ + template: ``, + moduleMetadata: { + imports: [BrowserAnimationsModule, ESChipsAutocompleteCheckboxModule] + } + }} + + + +## API + + + +## Constants + +Injection token to be used to override the default options for es-chips-autocomplete + +```ts +import { ES_CHIPS_DEFAULT_OPTIONS } from '@elonsoft/elonkit/chips-autocomplete'; + +@NgModule({ + providers: [ + { + provide: ES_CHIPS_DEFAULT_OPTIONS, + useValue: { + debounceTime: 1000, + freeInput: true, + unique: true, + selectable: true, + removable: true + } + } + ] +}) +``` diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/filter-options.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/filter-options.ts new file mode 100644 index 00000000..4debd28e --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/filter-options.ts @@ -0,0 +1,9 @@ +export function GetFilterOptions(text: string, options: any): any { + const lowerText = text ? text.toLowerCase() : ''; + return options.filter(e => e.toLowerCase().includes(lowerText)); +} + +export function GetFilterOptionsByKey(text: string, options: any, key: string): any { + const lowerText = text ? text.toLowerCase() : ''; + return options.filter(e => e[key].toLowerCase().includes(lowerText)); +} diff --git a/projects/elonkit/src/lib/ui/chips-autocomplete/index.ts b/projects/elonkit/src/lib/ui/chips-autocomplete/index.ts new file mode 100644 index 00000000..158da72c --- /dev/null +++ b/projects/elonkit/src/lib/ui/chips-autocomplete/index.ts @@ -0,0 +1,3 @@ +export { ESChipsAutocompleteModule } from './chips-autocomplete.module'; +export { ChipsAutocompleteComponent } from './chips-autocomplete.component'; +export { GetFilterOptions, GetFilterOptionsByKey } from './filter-options'; diff --git a/tsconfig.json b/tsconfig.json index 219429bf..da59973f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compileOnSave": false, "compilerOptions": { + "emitDecoratorMetadata": true, "baseUrl": "./", "outDir": "./dist/out-tsc", "sourceMap": true, diff --git a/tslint.json b/tslint.json index b8f1eb1e..8507e22e 100644 --- a/tslint.json +++ b/tslint.json @@ -12,12 +12,7 @@ "max-classes-per-file": false, "max-line-length": [true, 140], "member-access": false, - "member-ordering": [ - true, - { - "order": ["static-field", "instance-field", "static-method", "instance-method"] - } - ], + "member-ordering": [false], "no-consecutive-blank-lines": false, "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], "no-empty": false,