• 基于 Angular和Material autocomplete组件再封装的可双向绑定key-value的可输入下拉框



    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();
     67 
     68   myControl = new FormControl('');
     69   filteredOptions!: Observable;
     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();
    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();
    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: 打开关闭下拉选项框
     
     

     

  • 相关阅读:
    人工神经网络优化算法,人工智能神经网络模型
    船舶单独安装的双频GNSS的PPP解算
    立创 EDA #学习笔记10# | 常用连接器元器件识别 和 无源蜂鸣器驱动电路
    安装spark并配置高可用
    Ansible 自动化运维工具的使用
    为什么原生IP可以降低Google play账号关联风险?企业号解决8.3/10.3账号关联问题?
    【C++篇】AVL树
    linux PELT算法中的load计算
    安装配置Anaconda3
    【服务器部署篇】Linux下Tomcat安装和配置
  • 原文地址:https://www.cnblogs.com/grom/p/17754041.html