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自定义服务说明弹窗弹框 自下而上底部弹框

    前端Vue自定义服务说明弹窗弹框 自下而上底部弹框, 请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=13108 效果图如下: cc-serv ...

  2. Python潮流周刊#8:Python 3.13 计划将解释器提速 50%!

    你好,我是猫哥.这里每周分享优质的 Python 及通用技术内容,部分为英文,已在小标题注明.(标题取自其中一则分享,不代表全部内容都是该主题,特此声明.) 首发于我的博客:https://pytho ...

  3. Linux 上的 .NET 如何自主生成 Dump

    一:背景 1. 讲故事 前几天微信上有位朋友找到我,说他程序的 线程数 会偶发性瞬时飙高,让我看下大概是什么原因,截图如下: 如果这种问题每天都会出现,比较好的做法就是用 dotnet-trace 捕 ...

  4. 阿里云 MongoDB 创建库添加用户并授权

    先通过 root 进到 admin 库, 右击test 选择用户管理 测试连接

  5. win10系统网络图标变成一个地球模型并且无法连上网络

    最近在家远程办公,但是遇到个很棘手的问题,电脑突然连不上无线网络了.... 无线网络图标变成地球模型如下:

  6. Linux中常用数据库管理系统之MariaDB

    Linux中常用数据库管理系统之MariaDB 我们生活在信息化时代,经常要跟数据打交道,它在我们的日常生活中无处不在,比如手机支付,微信聊天,淘宝购物,使用的这些在后台都会对应一个叫数据库的存在.数 ...

  7. OpenCV计算机视觉学习(14)——浅谈常见图像后缀(png, jpg, bmp)的区别(opencv读取语义分割mask的坑)

    如果需要处理的原图及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 本来 ...

  8. MarkdownQuote:简化 Markdown 中的代码引用!

    MarkdownQuote:简化 Markdown 中的代码引用! 这是 SourceCodeTrace 项目之一,通过在 IDE 中提供一种便捷的方式,快速复制包含代码来源 Markdown 代码块 ...

  9. 1.简述Hibernate的工作原理。

    (1).首先,Configuration读取Hibernate的配置文件和映射文件中的信息,即加载配置文件和映射文件,并通过Hibernate配置文件生成一个多线程的SessionFactory对象: ...

  10. 2021-8-5 Microsoft文档学习笔记(C#)

    以下列表概述了类可以包含的成员类型. 常量:与类相关联的常量值 字段:与类关联的变量 方法:类可执行的操作 属性:与读取和写入类的已命名属性相关联的操作 索引器:与将类实例编入索引(像处理数组一样)相 ...