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的双向绑定和自定义选项的功能。
必传参数: