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. 前端vue uni-app列表组件 list组件,简单好用

    快速实现uni-app列表组件 list组件,简单好用; 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=12675 效果图如下: ...

  2. 2. Tomcat-Servlet

    1. Tomcat ‍ ​​ ‍ 目录结构说明: bin 可执行文件目录 conf 配置文件目录 lib 存放 lib 的目录 logs 日志文件目录 webapps 项目部署的目录 work 工作目 ...

  3. (一)centos7下如何搭建Nginx和FastDFS文件管理-环境搭建

    一.关于FastDFS 1.FastDFS简介 FastDFS(Fast Distributed File System)是一个开源的分布式文件系统,旨在解决大规模文件存储和访问的问题,例如图片.音视 ...

  4. .NET Core 3.1使用docker打包并部署

    目录 简介 环境介绍 开发环境 部署环境 编写Dockerfile文件 生成Docker镜像 运行容器 访问接口 结语 简介 本文主要说明使用.NET Core 3.1搭建的站点如何使用docker打 ...

  5. Django message组件

    使用message组件要在seetings中配置 ①INSTALLED_APPS   (项目需要什么功能都放在这 既可以有数据库,又可以写代码,html文件,和自己写的APP一个道理) ②MIDDLE ...

  6. 可托拉拽的WPF选项卡控件,强大好用!

    推荐一个简单易用的WPF选项卡控件. 项目简介 这是一个基于WPF开发的,可扩展.高度可定制.轻量级的UI组件,支持拖拉拽功能,可以让开发人员快速实现需要选项卡窗口的系统. 特色功能 1.拖拉拽标签: ...

  7. JFrame一些基础小知识

    JFrame.setLocationRelativeTo方法 JFrame.setLocationRelativeTo()是一个Java Swing中的方法,它用于将窗口居中显示在屏幕上. 当你调用该 ...

  8. Builder 生成器模式简介与 C# 示例【创建型2】【设计模式来了_2】

    〇.简介 1.什么是生成器模式? 一句话解释:   在构造一个复杂的对象(参数多且有可空类型)时,通过一个统一的构造链路,可选择的配置所需属性值,灵活实现可复用的构造过程. 生成器模式的重心,在于分离 ...

  9. quarkus依赖注入之六:发布和消费事件

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<quarkus依赖注入> ...

  10. Java源代码是如何编译,加载到内存中的?

    1.前言 相信许多开发同学看过<深入理解java虚拟机>,也阅读过java虚拟机规范,书籍和文档给人的感觉不够直观,本文从一个简单的例子来看看jvm是如何工作的吧. 本文所有操作均在mac ...