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的可输入下拉框的更多相关文章

  1. 从后台绑定数据到ligerui 的comboBox下拉框组件

    这次来记录一下ligerUI的comboBox下拉框组件,ligerUI的API里也有相关描写叙述,上面都是前台写死数据,然后显示在组件中,我这次要说的是将后台的数据绑定到下拉框组件中,废话不多说. ...

  2. 基于element-ui的多选下拉框和tag标签的二次封装

    前言: 今年这大半年我主要负责公司的后台教务管理的开发,这个管理系统目前主要是给公司的内部人员去配置公司的核心项目(例如:熊猫小课)的所有数据,例如课程的配置.课程期数的配置.课程版本的配置.活动的配 ...

  3. 基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用

    在我们实际项目开发过程中,往往需要根据实际情况,对组件进行封装,以更简便的在界面代码中使用,在实际的前端应用中,适当的组件封装,可以减少很多重复的界面代码,并且能够非常简便的使用,本篇随笔介绍基于El ...

  4. 手把手教学~基于element封装tree树状下拉框

    在日常项目开发中,树状下拉框的需求还是比较常见的,但是element并没有这种组件以供使用.在这里,小编就基于element如何封装一个树状下拉框做个详细的介绍. 通过这篇文章,你可以了解学习到一个树 ...

  5. 基于Bootstrap的下拉框插件bootstrap-select

    写在前面: 在这次的项目中,没有再使用liger-ui做为前端框架了,改为了Bootstrap,这次也好接触下新的技术,在学习的过程中发现,Bootstrap的一些组件基本都是采用class的形式,就 ...

  6. 自定义Angular指令与jQuery实现的Bootstrap风格数据双向绑定的单选&多选下拉框

    先说点闲话,熟悉Angular的猿们会喜欢这个插件的. 00.本末倒置 不得不承认我是一个喜欢本末倒置的人,学生时代就喜欢先把晚交的作业先做,留着马上就要交的作业不做,然后慢悠悠做完不重要的作业,卧槽 ...

  7. Combo( 自定义下拉框) 组件

    本节课重点了解 EasyUI 中 Combo(自定义下拉框)组件的使用方法,这个组件依赖于ValidateBox(验证框)组件 一. 加载方式自定义下拉框不能通过标签的方式进行创建.<input ...

  8. 第二百一十二节,jQuery EasyUI,Combo(自定义下拉框)组件

    jQuery EasyUI,Combo(自定义下拉框)组件 学习要点: 1.加载方式 2.属性列表 3.事件列表 4.方法列表 本节课重点了解 EasyUI 中 Combo(自定义下拉框)组件的使用方 ...

  9. vue2.X props 数据传递 实现组件内数据与组件外的数据的双向绑定

    vue2.0 禁止 子组件修改父组件数据 在Vue2中组件的props的数据流动改为了只能单向流动,即只能由组件外(调用组件方)通过组件的DOM属性attribute传递props给组件内,组件内只能 ...

  10. chosen组件实现下拉框

    chosen组件用于增强原生的select控件,使之有更好的用户体验.官方demo https://harvesthq.github.io/chosen/ 目前项目中碰到的使用,比如一个页面中有两个不 ...

随机推荐

  1. MyBatis体系笔记

    MyBatis 什么是MyBatis MyBatis是优秀的持久层框架 MyBatis使用XML将SQL与程序解耦,便于维护 MyBatis学习简单,执行高效,是JDBC的延伸 1.MyBatis开发 ...

  2. smarty 拼接字符串

    smarty 拼接字符串 newstr = [oldstr]|cat:[appendstr] 1 {$name = "Tom"} 2 {$phone = "1381234 ...

  3. sharding-jdbc分库连接数优化

    一.背景: 配运平台组的快递订单履约中心(cp-eofc)及物流平台履约中心(jdl-uep-ofc)系统都使用了ShardingSphere生态的sharding-jdbc作为分库分表中间件, 整个 ...

  4. 基于 NNCF 和 🤗 Optimum 面向 Intel CPU 对 Stable Diffusion 优化

    基于隐空间的扩散模型 (Latent Diffusion Model),是解决文本到图片生成问题上的颠覆者.Stable Diffusion 是最著名的一例,广泛应用在商业和工业.Stable Dif ...

  5. YOLOX目标检测实战:LabVIEW+YOLOX ONNX模型实现推理检测(含源码)

    目录 前言 一.什么是YOLOX 二.环境搭建 1.部署本项目时所用环境: 2.LabVIEW工具包下载及安装: 三.模型的获取与转化[推荐方式一] 1.方式一:直接在官网下载yolox的onnx模型 ...

  6. 用googletest写cpp单测

    框架概述 Google Test(也称为 googletest)是由 Google 开发的 C++ 单元测试框架.它的首个版本是在2004年发布的,作为 Google 内部的测试框架使用.随后,Goo ...

  7. log4j2---基于vulhub的log4j2漏洞复现---反弹shell

    基于vulhub的log4j2漏洞复现---反弹shell 1.方法一 环境准备: 和我上一篇fastjson1.2.24漏洞复现是一样的环境,方法也差别不大 声明:遵纪守法,仅作学习记录用处,部分描 ...

  8. Cilium系列-13-启用XDP加速及Cilium性能调优总结

    系列文章 Cilium 系列文章 前言 将 Kubernetes 的 CNI 从其他组件切换为 Cilium, 已经可以有效地提升网络的性能. 但是通过对 Cilium 不同模式的切换/功能的启用, ...

  9. 超详细的webpack之开始体验吧

    webpack是一个前端工程化非常重要静态模块化打包工具,可以帮我们把 less.sass.esmodule.commonjs 等模块依赖处理成浏览器可识别的静态资源. 虽然webpack非常好用,但 ...

  10. CSS基础(4)

    目录 1 定位 1.1 为什么需要定位 1.2 定位组成 1.2.1 边偏移(方位名词) 1.2.2 定位模式 (position) 1.3 定位模式介绍 1.3.1 静态定位(static) - 了 ...