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

基于,angular,material,autocomplete,组件,封装,双向,绑定,key,value,输入,下拉框 · 浏览次数 : 8

小编点评

**代码实现** ```javascript menuItems = ...; // 下拉框选项 model = ...; // 绑定变量后控件值 value = ...; // 输入或下拉选项中的实际值 disabled = ...; // 是否禁用控件 // 防抖函数 setTimeout(() => { // 选择项后触发两次Change事件 selelctoption(); modelChange(); }, 0.1); // 控制输入内容的正则表达式 const reg = /^[\w\s]+$/; // 输入内容与下拉菜单项匹配,匹配规则可以修改这里控制panelAction: 打开关闭下拉选项框 panelAction = match => reg.test(match.value); // 生成内容 generateContent = () => { // ...其他逻辑 ... // 将输入值赋到value变量中 value = inputContent; // 检查是否禁用控件 if (disabled) { model = inputValue; } // 控制输入内容的正则表达式匹配 panelAction = match => reg.test(match.value); // 更新模型值 model = panelAction(inputContent); }; ``` ****功能描述** * 生成文本内容 * 设置下拉菜单选项 * 设置绑定变量值 * 监听事件并处理逻辑 * 控制输入内容的正则表达式匹配 * 支持多事件处理 * 防抖函数确保事件触发时正确处理 ****其他** * 该代码使用了防抖函数,确保在选择项后只触发一次Change事件。 * 通过控制 `disabled` 属性,可以控制控件是否禁用。 * 通过使用 `panelAction` 函数,可以根据不同的条件控制输入内容。

正文


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: 打开关闭下拉选项框
 
 

 

与基于 Angular和Material autocomplete组件再封装的可双向绑定key-value的可输入下拉框相似的内容:

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

GitHub: https://github.com/Xinzheng-Li/AngularCustomerComponent 效果图:为了方便使用,把许多比如ADD的功能去了,可以在使用后自行实现。 调用: 1

【Dotnet 工具箱】基于 .NET 6 和 Angular 构建项目任务管理平台

1.Reha 时间管理大师 Rhea 是一个基于 C# 和 .NET 6 开发的在线任务管理平台,类似于 禅道、Jira、Redmine, 滴答清单等。 支持多视图多维度统一管理任务。多级结构,工作区,空间,文件夹,列表,可以更灵活的进行任务管理。 应用支持多主题和主题色切换,灵活搭配,随心所欲。

瑞亚时间管理大师,基于 .NET 6 和 Angular 构建的在线任务管理协作平台

瑞亚时间管理大师 瑞亚时间管理大师, 是一个在线的任务管理、项目管理、 团队协作平台。瑞亚 拥有现代化的页面风格,高效、简便,同时适合个人和团队使用。 瑞亚对个人免费,提供了无限制的任务,列表,和空间。 功能预览 瑞亚时间管理大师是以任务管理为核心,还包括了看板,文档,思维导图,白板,和 OKR 目

基于 Three.js 的 3D 模型加载优化

作为一个3D的项目,从用户打开页面到最终模型的渲染加载的时间也会比普通的H5项目要更长一些,从而造成大量的用户流失。为了提升首屏加载的转化率,需要尽可能的降低loading的时间。这里就分享一些我们在模型加载优化方面的心得。

基于MindSpore实现BERT对话情绪识别

本文分享自华为云社区《【昇思25天学习打卡营打卡指南-第二十四天】基于 MindSpore 实现 BERT 对话情绪识别》,作者:JeffDing。 模型简介 BERT全称是来自变换器的双向编码器表征量(Bidirectional Encoder Representations from Trans

基于 Vagrant 手动部署多个 Redis Server

环境准备 宿主机环境:Windows 10 虚拟机环境:Vagrant + VirtualBox Vagrantfile 配置 首先,我们需要编写一个 Vagrantfile 来定义我们的虚拟机配置。假设已经在 D:\Vagrant\redis 目录下创建了一个 Vagrantfile,其内容如下:

基于EF Core存储的Serilog持久化服务

前言 Serilog是 .NET 上的一个原生结构化高性能日志库,这个库能实现一些比内置库更高度的定制。日志持久化是其中一个非常重要的功能,生产环境通常很难挂接调试器或者某些bug的触发条件很奇怪。为了在脱离调试环境的情况下尽可能保留更多线索来辅助解决生产问题,持久化的日志就显得很重要了。目前Ser

基于EF Core存储的国际化服务

前言 .NET 官方有一个用来管理国际化资源的扩展包Microsoft.Extensions.Localization,ASP.NET Core也用这个来实现国际化功能。但是这个包的翻译数据是使用resx资源文件来管理的,这就意味着无法动态管理。虽然官方有在文档中提供了一些第三方管理方案,但是都不太

基于FileZilla上传、下载服务器数据的方法

本文介绍FileZilla软件的下载、配置与使用方法。 在之前的博客中,我们提到了下载高分遥感影像数据需要用到FTP(文件传输协议,File Transfer Protocol)软件FileZilla;这一软件用以在自己的电脑与服务器之间相互传输数据,在进行下载科学数据、网站开发等等操作时,经常需要

Vite5+Electron聊天室|electron31跨平台仿微信EXE客户端|vue3聊天程序

基于electron31+vite5+pinia2跨端仿微信Exe聊天应用ViteElectronChat。 electron31-vite5-chat原创研发vite5+electron31+pinia2+element-plus跨平台实战仿微信客户端聊天应用。实现了聊天、联系人、收藏、朋友圈/短