GitHub: https://github.com/Xinzheng-Li/AngularCustomerComponent
效果图:为了方便使用,把许多比如ADD的功能去了,可以在使用后自行实现。
调用:
1 <app-autocomplete-input [menuItems]="autocompleteInputData" [(model)]="autocompleteInputModel" [showAddBtn]="true"
2 [(value)]="autocompleteInputValue" (objectChange)="onChange($event)" (focus)="onFocus($event)"
3 (input)="onInput($event)" (change)="onModelChange($event)" (blur)="onBlur($event)"
4 #autocompleteInput></app-autocomplete-input>
前端:
1 <div>
2 <input type="text" matInput [formControl]="myControl"
3 #autocompleteTrigger="matAutocompleteTrigger" [matAutocomplete]="auto" [placeholder]="placeholder"
4 #autocompleteInput maxlength={{maxlength}} (focus)="onFocus($event)" (input)="onInput($event)"
5 (change)="onModelChange($event)" (blur)="onBlur($event)">
6
7 <mat-autocomplete #auto="matAutocomplete" #autocomplete isDisabled="true" (optionSelected)="selectedOption($event)"
8 [displayWith]="displayFn">
9 <mat-option *ngFor="let option of filteredOptions | async" [value]="option">
10 {{option.label}}
11 </mat-option>
12 <mat-option *ngIf="loading" [disabled]="true" class="loading">
13 loading...
14 </mat-option>
15 <mat-option *ngIf="showAddBtn&&inputText!=''" [ngClass]="{'addoption-active':addoptionActive}"
16 [disabled]="!addoptionActive" value="(add)" class="addoption">
17 + Add <span>{{ inputText?'"'+inputText+'"':inputText }}</span>
18 </mat-option>
19 </mat-autocomplete>
20 </div>
后台:
1 import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
2 import { FormControl } from '@angular/forms';
3 import { MatAutocomplete, MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete'
4 import { Observable, Subject, debounceTime, map, startWith } from 'rxjs';
5
6 interface Menu {
7 value: any;
8 label: string;
9 }
10
11 @Component({
12 selector: 'app-autocomplete-input',
13 templateUrl: './autocomplete-input.component.html',
14 styleUrls: ['./autocomplete-input.component.scss']
15 })
16 export class AutocompleteInputComponent implements OnInit {
17 @Input() disabled = false;
18 @Input() disabledInput = false;
19 @Input() placeholder = 'autocompleteInput';
20 @Input() maxlength: number = 50;
21 @Input() showAddBtn = false;
22 @Input() loading = false;
23 _menuItems!: Menu[];
24 @Input()
25 get menuItems() {
26 return this._menuItems;
27 }
28 set menuItems(val) {
29 this._menuItems = val;
30 if (this.model) {
31 let mapItem = this.menuItems.find((x) => x.label?.toLowerCase().trim() == this.model?.trim()?.toLowerCase());
32 if (mapItem) {
33 this.value = mapItem.value;
34 } else {
35 this.model = this.value = '';
36 }
37 }
38 this.myControl.setValue(this.model ?? '');
39 }
40
41 modelValue: any = { name: '', value: '' };
42 @Output() objectChange = new EventEmitter();
43
44 //Only for binding model
45 @Output() modelChange = new EventEmitter();
46 @Input()
47 get model() {
48 return this.modelValue?.name?.trim() ?? '';
49 }
50 set model(val) {
51 this.modelValue.name = this.inputText = val?.trim();
52 this.modelChange.emit(this.modelValue.name);
53 this.inputChangeSubject.next(this.modelValue.name);
54 }
55
56 @Output() valueChange = new EventEmitter();
57 @Input()
58 get value() {
59 return this.modelValue.value;
60 }
61 set value(val) {
62 this.modelValue.value = val;
63 this.valueChange.emit(this.modelValue.value);
64 }
65
66 @Output() inputChange = new EventEmitter<any>();
67
68 myControl = new FormControl<string | any>('');
69 filteredOptions!: Observable<any[]>;
70 @ViewChild('autocompleteInput') autocompleteInput: any;
71 @ViewChild('autocomplete') autocomplete!: MatAutocomplete;
72 @ViewChild(MatAutocompleteTrigger) autocompleteTrigger!: MatAutocompleteTrigger;
73
74 ngOnInit(): void {
75 this.filteredOptions = this.myControl.valueChanges.pipe(
76 startWith(''),
77 map((value) => {
78 const name = typeof value === 'string' ? value : value.label;
79 return name ? this._filter(name as string) : this.menuItems.slice();
80 })
81 );
82 this.registEventSubject();
83 this.inputText = '';
84 }
85
86 ngOnChanges(changes: SimpleChanges) {
87 if (changes['menuItems'] && !changes['menuItems'].firstChange) this.loading = false;
88 if (changes['disabled']) {
89 this.disabled ? this.myControl.disable() : this.myControl.enable();
90 }
91 if (changes['value']) {
92 let item = this.menuItems.find((x) => x.value == changes['value'].currentValue);
93 if (item) {
94 this.value = item?.value ?? '';
95 this.model = item?.label ?? '';
96 }
97 }
98 if (changes['model']) {
99 this.inputText = changes['model'].currentValue ?? '';
100 this.myControl.setValue(this.model ?? '');
101 }
102 }
103
104 private inputChangeSubject = new Subject<string>();
105 private registEventSubject() {
106 this.inputChangeSubject.pipe(debounceTime(100)).subscribe((data: any) => {
107 if (this.loading) return;
108 if (this.autocompleteInput?.nativeElement) this.autocompleteInput.nativeElement.value = this.model;
109 this.objectChange.emit(this.modelValue);
110 });
111 }
112
113 private _filter(item: any): any[] {
114 const filterValue = item?.toLowerCase()?.trim();
115 return this.menuItems.filter((option) => option.label.toLowerCase().includes(filterValue));
116 }
117
118 displayFn(e: any) {
119 return e && e.label ? e.label : '';
120 }
121 onFocus(e: any) {
122 if (this.disabledInput) e.target.blur();
123 }
124 @Output() blur = new EventEmitter<any>();
125 onBlur(e: any) {
126 if (e.currentTarget.value != this.model) {
127 this.inputChangeSubject.next(this.model);
128 } else {
129 this.blur.emit(e);
130 }
131 }
132
133 inputText = '';
134 addoptionActive = false;
135 onInput(e: any) {
136 if (e.currentTarget.value == '') {
137 this.addoptionAction(false);
138 this.myControl.setValue('');
139 } else if (this.menuItems.find((x) => x.label.toLowerCase() == e.currentTarget.value?.trim()?.toLowerCase())) {
140 this.addoptionAction(false);
141 } else {
142 this.addoptionAction(true);
143 }
144 this.inputText = e.currentTarget.value;
145 e.currentTarget.value = this.inputText = e.currentTarget.value.replaceAll(/[`\\~!@#$%^\*_\+={}\[\]\|;"<>\?]/gi, '');
146 if (e.currentTarget.value?.trim() == '') this.myControl.setValue(e.currentTarget.value);
147 this.inputChange.emit(e);
148 }
149
150 onModelChange(e: any) {
151 if (this.loading) return;
152 if (e.currentTarget.value?.trim()) {
153 let mapItem = this.menuItems.find(
154 (x) => x.label.toLowerCase().trim() == e.currentTarget.value?.trim()?.toLowerCase()
155 );
156 if (mapItem) {
157 this.model = e.currentTarget.value = mapItem.label;
158 this.value = mapItem.value;
159 } else {
160 this.model = e.currentTarget.value;
161 this.value = '';
162 }
163 } else {
164 this.model = this.inputText = e.currentTarget.value;
165 this.value = '';
166 }
167 }
168
169 selectedOption(e: any) {
170 if (typeof e.option.value === 'string') {
171 this.autocompleteInput.nativeElement.value = this.inputText;
172 } else {
173 let mod = e.option.getLabel() ?? '';
174 let val = e.option.value?.value ?? '';
175 if (val != this.value || mod != this.model) {
176 this.model = mod ?? '';
177 this.value = val ?? '';
178 }
179 if (this.value && this.model) {
180 this.addoptionActive = false;
181 }
182 }
183 }
184
185 panelAction(type: number) {
186 type == 1 ? this.autocompleteTrigger.openPanel() : this.autocompleteTrigger.closePanel();
187 }
188
189 addoptionAction(type: boolean) {
190 this.addoptionActive = type;
191 }
192
193 //It will trigger the change event of the model!
194 clearText() {
195 this.value = this.model = '';
196 }
197 }
实现逻辑:
原Material的autocomplete控件将下拉框和输入内容分为不同的事件,并且无法自定义下拉选项,像例子中的ADD功能,如果使用原控件,则会将"+ Add XXX"显示到输入框中。
另外就是原控件仅支持显示值绑定,因为输入框是没有key的,故,将输入框和下拉框进行二次封装,实现key-value的双向绑定和自定义选项的功能。
必传参数:
[menuItems]: 下拉框的选项,以value-label的形式定义。
[(model)]: 绑定变量后控件会将输入或下拉选项中的显示值赋到此变量,修改此变量也会更改输入框的值。
[(value)]: 绑定变量后控件会将输入或下拉选项中的实际值赋到此变量,如果是输入不在下拉框的中值,则此变量为空,可以根据需要自行实现生成value值。
可选参数:
[disabled]: 是否禁用控件
[disabledInput]: 是否禁止输入(下拉框可用)
[placeholder]: 输入框默认显示值
[maxlength]: 输入框最大长度
[showAddBtn]:是否显示添加项按钮(需要自己实现事件,比如生成个key之后push到menuItems中)
[loading]:当数据源为异步加载时,通过控制此变量来显示等待icon
(objectChange): 修改控件值后触发(选中下拉选项、改变或清空输入框值),输出参数为控件key,value, 由于前面已经对key value进行了双向绑定,事件触发不需要再次进行赋值。
其他事件**...**
其他:
106行:防抖函数0.1秒是因为选择项后会触发两次Change事件(selelctoption+modelChange)
145行:控制输入内容的正则表达式
31/139/154行:输入内容与下拉菜单项匹配,匹配规则可以修改这里控制
panelAction: 打开关闭下拉选项框