Skip to content

Commit b9cf937

Browse files
ypzhaofengtianze
authored andcommitted
fix: optimization tags input
1 parent 3c9aa47 commit b9cf937

File tree

4 files changed

+267
-5
lines changed

4 files changed

+267
-5
lines changed

src/input/tags-input/mixin.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
@mixin component-style($block) {
77
.#{$block} {
8-
display: inline-block;
8+
display: flex;
9+
flex-wrap: wrap;
910
position: relative;
1011
width: 100%;
1112
color: use-text-color(main);
@@ -115,6 +116,7 @@
115116
}
116117

117118
.aui-tag#{&}__input {
119+
flex: 1 0 auto;
118120
margin-right: 0;
119121
padding: 0;
120122
width: 1em;
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { Component } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
fakeAsync,
6+
tick,
7+
} from '@angular/core/testing';
8+
import {
9+
ReactiveFormsModule,
10+
FormControl,
11+
Validators,
12+
FormGroup,
13+
ValidatorFn,
14+
AbstractControl,
15+
AsyncValidatorFn,
16+
} from '@angular/forms';
17+
import { By } from '@angular/platform-browser';
18+
import { TagsInputComponent } from './tags-input.component';
19+
import { of } from 'rxjs';
20+
21+
describe('TagsInputComponent Required Validation Behavior', () => {
22+
let fixture: ComponentFixture<TestFormComponent>;
23+
let testHost: TestFormComponent;
24+
let inputEl: HTMLInputElement;
25+
let tagComp: TagsInputComponent;
26+
27+
beforeEach(() => {
28+
fixture = TestBed.createComponent(TestFormComponent);
29+
fixture.detectChanges();
30+
tagComp = fixture.debugElement.query(
31+
By.directive(TagsInputComponent),
32+
).componentInstance;
33+
testHost = fixture.componentInstance;
34+
const el = fixture.debugElement.query(
35+
By.css('.aui-tags-input'),
36+
).nativeElement;
37+
inputEl = el.querySelector('input');
38+
});
39+
40+
// it('should mark form valid when input has value but tag is not confirmed yet', fakeAsync(() => {
41+
// expect(testHost.form.valid).toBeFalsy();
42+
43+
// inputEl.value = 'hello';
44+
// inputEl.dispatchEvent(new Event('input'));
45+
// fixture.detectChanges();
46+
// tick();
47+
// fixture.detectChanges();
48+
49+
// expect(testHost.form.valid).toBeTruthy();
50+
51+
// inputEl.dispatchEvent(new Event('blur'));
52+
// fixture.detectChanges();
53+
// tick();
54+
// fixture.detectChanges();
55+
56+
// expect(testHost.form.valid).toBeTruthy();
57+
// expect(testHost.form.get('tags')!.value).toEqual(['hello']);
58+
// }));
59+
60+
describe('allowRepeat Behavior', () => {
61+
it('should NOT allow duplicate tags when allowRepeat = false', fakeAsync(() => {
62+
testHost.allowRepeat = false;
63+
fixture.detectChanges();
64+
65+
inputEl.value = 'a';
66+
inputEl.dispatchEvent(new Event('input'));
67+
inputEl.dispatchEvent(new Event('blur'));
68+
tick();
69+
fixture.detectChanges();
70+
71+
inputEl.value = 'a';
72+
inputEl.dispatchEvent(new Event('input'));
73+
inputEl.dispatchEvent(new Event('blur'));
74+
tick();
75+
fixture.detectChanges();
76+
77+
expect(testHost.form.get('tags')!.value).toEqual(['a']);
78+
}));
79+
80+
it('should allow duplicate tags when allowRepeat = true', fakeAsync(() => {
81+
testHost.allowRepeat = true;
82+
fixture.detectChanges();
83+
84+
inputEl.value = 'a';
85+
inputEl.dispatchEvent(new Event('input'));
86+
inputEl.dispatchEvent(new Event('blur'));
87+
tick();
88+
fixture.detectChanges();
89+
90+
inputEl.value = 'a';
91+
inputEl.dispatchEvent(new Event('input'));
92+
inputEl.dispatchEvent(new Event('blur'));
93+
tick();
94+
fixture.detectChanges();
95+
96+
expect(testHost.form.get('tags')!.value).toEqual(['a', 'a']);
97+
}));
98+
});
99+
100+
describe('allowEmpty Behavior', () => {
101+
it('should NOT add empty tag when allowEmpty = false', fakeAsync(() => {
102+
testHost.allowEmpty = false;
103+
fixture.detectChanges();
104+
105+
inputEl.value = '';
106+
inputEl.dispatchEvent(new Event('input'));
107+
inputEl.dispatchEvent(new Event('blur'));
108+
tick();
109+
fixture.detectChanges();
110+
111+
expect(testHost.form.get('tags')!.value).toEqual([]);
112+
}));
113+
114+
it('should add empty tag when allowEmpty = true', fakeAsync(() => {
115+
testHost.allowEmpty = true;
116+
fixture.detectChanges();
117+
118+
inputEl.value = '';
119+
inputEl.dispatchEvent(new Event('input'));
120+
inputEl.dispatchEvent(new Event('blur'));
121+
tick();
122+
fixture.detectChanges();
123+
124+
expect(testHost.form.get('tags')!.value).toEqual(['']);
125+
}));
126+
});
127+
128+
describe('inputValidator behavior', () => {
129+
it('should NOT add tag when input does NOT pass inputValidator', fakeAsync(() => {
130+
testHost.checkFn = control => {
131+
const value = control.value as string[];
132+
if (value.includes('a')) {
133+
return { patternB: true };
134+
}
135+
return null;
136+
};
137+
fixture.detectChanges();
138+
139+
inputEl.value = 'apple';
140+
inputEl.dispatchEvent(new Event('input'));
141+
inputEl.dispatchEvent(new Event('blur'));
142+
tick(0);
143+
fixture.detectChanges();
144+
145+
expect(tagComp.model).toEqual([]);
146+
expect(testHost.form.get('tags')!.value).toEqual([]);
147+
expect(testHost.form.valid).toBeFalsy();
148+
}));
149+
150+
it('should add tag when input passes inputValidator', fakeAsync(() => {
151+
testHost.checkFn = control => {
152+
const value = control.value as string[];
153+
if (value.includes('a')) {
154+
return { patternB: true };
155+
}
156+
return null;
157+
};
158+
inputEl.value = 'ccc';
159+
inputEl.dispatchEvent(new Event('input'));
160+
inputEl.dispatchEvent(new Event('blur'));
161+
tick(0);
162+
fixture.detectChanges();
163+
164+
expect(tagComp.model).toEqual(['ccc']);
165+
expect(testHost.form.get('tags')!.value).toEqual(['ccc']);
166+
expect(testHost.form.valid).toBeTruthy();
167+
}));
168+
});
169+
170+
describe('inputAsyncValidator behavior', () => {
171+
it('should block adding tag when async validator resolves to false', fakeAsync(() => {
172+
testHost.inputAsyncValidator = (_control: AbstractControl) =>
173+
of({ tagAsyncInvalid: true });
174+
fixture.detectChanges();
175+
176+
inputEl.value = 'bad';
177+
inputEl.dispatchEvent(new Event('input'));
178+
inputEl.dispatchEvent(new Event('blur'));
179+
180+
tick();
181+
fixture.detectChanges();
182+
183+
expect(testHost.form.get('tags')!.value).toEqual([]);
184+
}));
185+
186+
it('should allow adding tag when async validator resolves to true', fakeAsync(() => {
187+
testHost.inputAsyncValidator = (_control: AbstractControl) =>
188+
Promise.resolve(null);
189+
fixture.detectChanges();
190+
191+
inputEl.value = 'hello';
192+
inputEl.dispatchEvent(new Event('input'));
193+
inputEl.dispatchEvent(new Event('blur'));
194+
195+
tick();
196+
fixture.detectChanges();
197+
198+
expect(testHost.form.get('tags')!.value).toEqual(['hello']);
199+
}));
200+
});
201+
});
202+
203+
@Component({
204+
template: `
205+
<form [formGroup]="form">
206+
<aui-tags-input
207+
required
208+
formControlName="tags"
209+
[allowRepeat]="allowRepeat"
210+
[allowEmpty]="allowEmpty"
211+
[inputValidator]="checkFn"
212+
[inputAsyncValidator]="inputAsyncValidator"
213+
></aui-tags-input>
214+
</form>
215+
`,
216+
imports: [ReactiveFormsModule, TagsInputComponent],
217+
})
218+
class TestFormComponent {
219+
allowRepeat = false;
220+
allowEmpty = false;
221+
222+
inputAsyncValidator: AsyncValidatorFn = (_control: AbstractControl) =>
223+
Promise.resolve(null);
224+
225+
checkFn: ValidatorFn = () => {
226+
return null;
227+
};
228+
229+
form = new FormGroup({
230+
tags: new FormControl<string[]>([], Validators.required),
231+
});
232+
}

src/input/tags-input/tags-input.component.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<div
22
[class]="rootClass"
3-
(mousedown)="$event.preventDefault()"
43
(click)="inputRef.focus()"
54
[style.max-height]="maxHeight"
65
>

stories/input/tags-input.component.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChangeDetectionStrategy, Component } from '@angular/core';
2-
import { FormControl, ValidatorFn } from '@angular/forms';
2+
import { FormControl, ValidatorFn, Validators } from '@angular/forms';
33

44
import { ComponentSize } from '@alauda/ui';
55

@@ -9,7 +9,6 @@ import { ComponentSize } from '@alauda/ui';
99
<aui-tags-input
1010
[size]="size"
1111
[formControl]="control"
12-
[inputValidator]="checkFn"
1312
[clearable]="true"
1413
[allowRepeat]="allowRepeat"
1514
[allowEmpty]="allowEmpty"
@@ -33,12 +32,35 @@ import { ComponentSize } from '@alauda/ui';
3332
[allowEmpty]="allowEmpty"
3433
placeholder="placeholder"
3534
></aui-tags-input>
35+
<br />
36+
<br />
37+
38+
<div>多行 tags maxRowCount: {{ maxRowCount }}</div>
39+
<aui-tags-input
40+
style="width: 400px;"
41+
[size]="size"
42+
[formControl]="rowsControl"
43+
[inputValidator]="checkFn"
44+
[maxRowCount]="maxRowCount"
45+
[clearable]="true"
46+
[readonlyTags]="['service']"
47+
[allowRepeat]="allowRepeat"
48+
[allowEmpty]="allowEmpty"
49+
placeholder="placeholder"
50+
></aui-tags-input>
3651
`,
3752
changeDetection: ChangeDetectionStrategy.OnPush,
3853
standalone: false,
3954
})
4055
export class TagsInputComponent {
4156
value = ['app', 'service'];
57+
rowsValue = [
58+
'app123456789123456789123456789',
59+
'service',
60+
'db23456789123456789123456789',
61+
'zxcvbnmasdfghjkqwertyuiop',
62+
];
63+
maxRowCount = 2;
4264
pattern = /^a/;
4365
sizeOptions = {
4466
[ComponentSize.Large]: ComponentSize.Large,
@@ -55,7 +77,14 @@ export class TagsInputComponent {
5577
return null;
5678
};
5779

58-
control = new FormControl(this.value, { validators: [this.checkArrFn] });
80+
control = new FormControl(this.value, {
81+
validators: [Validators.required, this.checkArrFn],
82+
});
83+
84+
rowsControl = new FormControl(this.rowsValue, {
85+
validators: [this.checkArrFn],
86+
});
87+
5988
size = ComponentSize.Medium;
6089
allowRepeat = true;
6190
allowEmpty = false;

0 commit comments

Comments
 (0)