From a927539e5aa548e3cf5bb752e03b326e952ea80d Mon Sep 17 00:00:00 2001 From: ypzhao Date: Wed, 12 Nov 2025 14:40:39 +0800 Subject: [PATCH 1/2] fix: optimization tags input --- src/input/tags-input/mixin.scss | 4 +- .../tags-input-form.component.spec.ts | 232 ++++++++++++++++++ .../tags-input/tags-input.component.html | 1 - stories/input/tags-input.component.ts | 35 ++- 4 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 src/input/tags-input/tags-input-form.component.spec.ts diff --git a/src/input/tags-input/mixin.scss b/src/input/tags-input/mixin.scss index 0a5346cf1..6960b33bd 100644 --- a/src/input/tags-input/mixin.scss +++ b/src/input/tags-input/mixin.scss @@ -5,7 +5,8 @@ @mixin component-style($block) { .#{$block} { - display: inline-block; + display: flex; + flex-wrap: wrap; position: relative; width: 100%; color: use-text-color(main); @@ -115,6 +116,7 @@ } .aui-tag#{&}__input { + flex: 1 0 auto; margin-right: 0; padding: 0; width: 1em; diff --git a/src/input/tags-input/tags-input-form.component.spec.ts b/src/input/tags-input/tags-input-form.component.spec.ts new file mode 100644 index 000000000..9036a93be --- /dev/null +++ b/src/input/tags-input/tags-input-form.component.spec.ts @@ -0,0 +1,232 @@ +import { Component } from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { + ReactiveFormsModule, + FormControl, + Validators, + FormGroup, + ValidatorFn, + AbstractControl, + AsyncValidatorFn, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { TagsInputComponent } from './tags-input.component'; +import { of } from 'rxjs'; + +describe('TagsInputComponent Required Validation Behavior', () => { + let fixture: ComponentFixture; + let testHost: TestFormComponent; + let inputEl: HTMLInputElement; + let tagComp: TagsInputComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestFormComponent); + fixture.detectChanges(); + tagComp = fixture.debugElement.query( + By.directive(TagsInputComponent), + ).componentInstance; + testHost = fixture.componentInstance; + const el = fixture.debugElement.query( + By.css('.aui-tags-input'), + ).nativeElement; + inputEl = el.querySelector('input'); + }); + + // it('should mark form valid when input has value but tag is not confirmed yet', fakeAsync(() => { + // expect(testHost.form.valid).toBeFalsy(); + + // inputEl.value = 'hello'; + // inputEl.dispatchEvent(new Event('input')); + // fixture.detectChanges(); + // tick(); + // fixture.detectChanges(); + + // expect(testHost.form.valid).toBeTruthy(); + + // inputEl.dispatchEvent(new Event('blur')); + // fixture.detectChanges(); + // tick(); + // fixture.detectChanges(); + + // expect(testHost.form.valid).toBeTruthy(); + // expect(testHost.form.get('tags')!.value).toEqual(['hello']); + // })); + + describe('allowRepeat Behavior', () => { + it('should NOT allow duplicate tags when allowRepeat = false', fakeAsync(() => { + testHost.allowRepeat = false; + fixture.detectChanges(); + + inputEl.value = 'a'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(); + fixture.detectChanges(); + + inputEl.value = 'a'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(); + fixture.detectChanges(); + + expect(testHost.form.get('tags')!.value).toEqual(['a']); + })); + + it('should allow duplicate tags when allowRepeat = true', fakeAsync(() => { + testHost.allowRepeat = true; + fixture.detectChanges(); + + inputEl.value = 'a'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(); + fixture.detectChanges(); + + inputEl.value = 'a'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(); + fixture.detectChanges(); + + expect(testHost.form.get('tags')!.value).toEqual(['a', 'a']); + })); + }); + + describe('allowEmpty Behavior', () => { + it('should NOT add empty tag when allowEmpty = false', fakeAsync(() => { + testHost.allowEmpty = false; + fixture.detectChanges(); + + inputEl.value = ''; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(); + fixture.detectChanges(); + + expect(testHost.form.get('tags')!.value).toEqual([]); + })); + + it('should add empty tag when allowEmpty = true', fakeAsync(() => { + testHost.allowEmpty = true; + fixture.detectChanges(); + + inputEl.value = ''; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(); + fixture.detectChanges(); + + expect(testHost.form.get('tags')!.value).toEqual(['']); + })); + }); + + describe('inputValidator behavior', () => { + it('should NOT add tag when input does NOT pass inputValidator', fakeAsync(() => { + testHost.checkFn = control => { + const value = control.value as string[]; + if (value.includes('a')) { + return { patternB: true }; + } + return null; + }; + fixture.detectChanges(); + + inputEl.value = 'apple'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(0); + fixture.detectChanges(); + + expect(tagComp.model).toEqual([]); + expect(testHost.form.get('tags')!.value).toEqual([]); + expect(testHost.form.valid).toBeFalsy(); + })); + + it('should add tag when input passes inputValidator', fakeAsync(() => { + testHost.checkFn = control => { + const value = control.value as string[]; + if (value.includes('a')) { + return { patternB: true }; + } + return null; + }; + inputEl.value = 'ccc'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + tick(0); + fixture.detectChanges(); + + expect(tagComp.model).toEqual(['ccc']); + expect(testHost.form.get('tags')!.value).toEqual(['ccc']); + expect(testHost.form.valid).toBeTruthy(); + })); + }); + + describe('inputAsyncValidator behavior', () => { + it('should block adding tag when async validator resolves to false', fakeAsync(() => { + testHost.inputAsyncValidator = (_control: AbstractControl) => + of({ tagAsyncInvalid: true }); + fixture.detectChanges(); + + inputEl.value = 'bad'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + + tick(); + fixture.detectChanges(); + + expect(testHost.form.get('tags')!.value).toEqual([]); + })); + + it('should allow adding tag when async validator resolves to true', fakeAsync(() => { + testHost.inputAsyncValidator = (_control: AbstractControl) => + Promise.resolve(null); + fixture.detectChanges(); + + inputEl.value = 'hello'; + inputEl.dispatchEvent(new Event('input')); + inputEl.dispatchEvent(new Event('blur')); + + tick(); + fixture.detectChanges(); + + expect(testHost.form.get('tags')!.value).toEqual(['hello']); + })); + }); +}); + +@Component({ + template: ` +
+ +
+ `, + imports: [ReactiveFormsModule, TagsInputComponent], +}) +class TestFormComponent { + allowRepeat = false; + allowEmpty = false; + + inputAsyncValidator: AsyncValidatorFn = (_control: AbstractControl) => + Promise.resolve(null); + + checkFn: ValidatorFn = () => { + return null; + }; + + form = new FormGroup({ + tags: new FormControl([], Validators.required), + }); +} diff --git a/src/input/tags-input/tags-input.component.html b/src/input/tags-input/tags-input.component.html index 8479f9a35..420620990 100644 --- a/src/input/tags-input/tags-input.component.html +++ b/src/input/tags-input/tags-input.component.html @@ -1,6 +1,5 @@
diff --git a/stories/input/tags-input.component.ts b/stories/input/tags-input.component.ts index c2296ad0c..9753b3e40 100644 --- a/stories/input/tags-input.component.ts +++ b/stories/input/tags-input.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { FormControl, ValidatorFn } from '@angular/forms'; +import { FormControl, ValidatorFn, Validators } from '@angular/forms'; import { ComponentSize } from '@alauda/ui'; @@ -9,7 +9,6 @@ import { ComponentSize } from '@alauda/ui'; +
+
+ +
多行 tags maxRowCount: {{ maxRowCount }}
+ `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, }) export class TagsInputComponent { value = ['app', 'service']; + rowsValue = [ + 'app123456789123456789123456789', + 'service', + 'db23456789123456789123456789', + 'zxcvbnmasdfghjkqwertyuiop', + ]; + maxRowCount = 2; pattern = /^a/; sizeOptions = { [ComponentSize.Large]: ComponentSize.Large, @@ -55,7 +77,14 @@ export class TagsInputComponent { return null; }; - control = new FormControl(this.value, { validators: [this.checkArrFn] }); + control = new FormControl(this.value, { + validators: [Validators.required, this.checkArrFn], + }); + + rowsControl = new FormControl(this.rowsValue, { + validators: [this.checkArrFn], + }); + size = ComponentSize.Medium; allowRepeat = true; allowEmpty = false; From 91212b84195143c95342fbdb054a75f0a9c38cf6 Mon Sep 17 00:00:00 2001 From: Tianze Feng Date: Wed, 12 Nov 2025 14:45:51 +0800 Subject: [PATCH 2/2] Optimize tags input in changeset --- .changeset/happy-planets-yell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/happy-planets-yell.md diff --git a/.changeset/happy-planets-yell.md b/.changeset/happy-planets-yell.md new file mode 100644 index 000000000..2ec346f8f --- /dev/null +++ b/.changeset/happy-planets-yell.md @@ -0,0 +1,5 @@ +--- +"@alauda/ui": patch +--- + +fix: optimize tags input