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 @@
+
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: `
+ `,
+ 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 @@
+
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: `
+ `,
+ 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 @@
+
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: `
+ `,
+ 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,