Skip to content

Commit 830b4b3

Browse files
feat(module:input): add nzAllowClear input and nzClear callback (#9452)
1 parent 10889d7 commit 830b4b3

File tree

7 files changed

+158
-48
lines changed

7 files changed

+158
-48
lines changed

components/input/demo/allow-clear.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,17 @@ import { NzInputModule } from 'ng-zorro-antd/input';
1010
template: `
1111
<nz-input-wrapper nzAllowClear>
1212
<input nz-input [(ngModel)]="inputValue" placeholder="input with clear icon" />
13-
<nz-icon
14-
nzInputSuffix
15-
class="ant-input-clear-icon"
16-
nzType="close-circle"
17-
nzTheme="fill"
18-
[hidden]="!inputValue"
19-
(click)="inputValue = null"
20-
/>
2113
</nz-input-wrapper>
2214
<br />
2315
<br />
24-
<nz-input-wrapper nzAllowClear class="ant-input-affix-wrapper-textarea-with-clear-btn">
16+
<nz-input-wrapper nzAllowClear>
17+
<input nz-input [(ngModel)]="inputValue" placeholder="input with custom clear icon" />
18+
<nz-icon nzInputClearIcon nzType="close" />
19+
</nz-input-wrapper>
20+
<br />
21+
<br />
22+
<nz-input-wrapper nzAllowClear>
2523
<textarea nz-input [(ngModel)]="textValue" placeholder="textarea with clear icon"></textarea>
26-
<nz-icon
27-
nzInputSuffix
28-
class="ant-input-clear-icon"
29-
nzType="close-circle"
30-
nzTheme="fill"
31-
[hidden]="!textValue"
32-
(click)="inputValue = null"
33-
/>
3424
</nz-input-wrapper>
3525
`
3626
})
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from '@angular/core';
1+
import { Component, signal } from '@angular/core';
22
import { FormsModule } from '@angular/forms';
33

44
import { NzButtonModule } from 'ng-zorro-antd/button';
@@ -10,15 +10,15 @@ import { NzInputModule } from 'ng-zorro-antd/input';
1010
imports: [FormsModule, NzButtonModule, NzInputModule, NzIconModule],
1111
template: `
1212
<nz-input-wrapper class="ant-input-search">
13-
<input nz-input type="search" placeholder="input search text" />
13+
<input nz-input [(ngModel)]="value" type="search" placeholder="input search text" />
1414
<button nzInputAddonAfter nz-button class="ant-input-search-button">
1515
<nz-icon nzType="search" />
1616
</button>
1717
</nz-input-wrapper>
1818
<br />
1919
<br />
2020
<nz-input-wrapper nzAllowClear class="ant-input-search">
21-
<input nz-input type="search" placeholder="input search text" />
21+
<input nz-input [(ngModel)]="value" type="search" placeholder="input search text" />
2222
<button nzInputAddonAfter nz-button class="ant-input-search-button">
2323
<nz-icon nzType="search" />
2424
</button>
@@ -27,36 +27,38 @@ import { NzInputModule } from 'ng-zorro-antd/input';
2727
<br />
2828
<nz-input-wrapper class="ant-input-search">
2929
<span nzInputAddonBefore>https://</span>
30-
<input nz-input type="search" placeholder="input search text" />
30+
<input nz-input [(ngModel)]="value" type="search" placeholder="input search text" />
3131
<button nzInputAddonAfter nz-button class="ant-input-search-button">
3232
<nz-icon nzType="search" />
3333
</button>
3434
</nz-input-wrapper>
3535
<br />
3636
<br />
3737
<nz-input-wrapper class="ant-input-search ant-input-search-with-button">
38-
<input nz-input type="search" placeholder="input search text" />
38+
<input nz-input [(ngModel)]="value" type="search" placeholder="input search text" />
3939
<button nzInputAddonAfter nz-button nzType="primary" class="ant-input-search-button">
4040
<nz-icon nzType="search" />
4141
</button>
4242
</nz-input-wrapper>
4343
<br />
4444
<br />
4545
<nz-input-wrapper class="ant-input-search ant-input-search-large ant-input-search-with-button">
46-
<input nz-input type="search" placeholder="input search text" nzSize="large" />
46+
<input nz-input [(ngModel)]="value" type="search" placeholder="input search text" nzSize="large" />
4747
<button nzInputAddonAfter nz-button nzType="primary" nzSize="large" class="ant-input-search-button"
4848
>Submit</button
4949
>
5050
</nz-input-wrapper>
5151
<br />
5252
<br />
5353
<nz-input-wrapper class="ant-input-search ant-input-search-large ant-input-search-with-button">
54-
<input nz-input type="search" placeholder="input search text" nzSize="large" />
54+
<input nz-input [(ngModel)]="value" type="search" placeholder="input search text" nzSize="large" />
5555
<nz-icon nzInputSuffix nzType="audio" [style.font-size.px]="16" [style.color]="'#1677ff'" />
56-
<button nzInputAddonAfter nz-button nzType="primary" nzSize="large" class="ant-input-search-button"
57-
>Submit</button
58-
>
56+
<button nzInputAddonAfter nz-button nzType="primary" nzSize="large" class="ant-input-search-button">
57+
Submit
58+
</button>
5959
</nz-input-wrapper>
6060
`
6161
})
62-
export class NzDemoInputSearchInputComponent {}
62+
export class NzDemoInputSearchInputComponent {
63+
readonly value = signal('');
64+
}

components/input/doc/index.en-US.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ All props of input supported by [w3c standards](https://www.w3schools.com/tags/t
2828

2929
### nz-input-wrapper
3030

31-
Used when you need to add prefixes and suffixes or pre- and post-tabs to `[nz-input]`.
32-
33-
| Property | Description | Type | Default |
34-
| ----------------- | --------------------------------------------------------------------- | -------- | ------- |
35-
| `[nzAddonBefore]` | The label text displayed before (on the left side of) the input field | `string` | - |
36-
| `[nzAddonAfter]` | The label text displayed after (on the right side of) the input field | `string` | - |
37-
| `[nzPrefix]` | The prefix icon for the Input | `string` | - |
38-
| `[nzSuffix]` | The suffix icon for the Input | `string` | - |
31+
Use when you need to add extra functionality to `[nz-input]`.
32+
33+
| Property | Description | Type | Default |
34+
| ----------------- | --------------------------------------------------------------------- | ------------------------ | ------- |
35+
| `[nzAddonBefore]` | The label text displayed before (on the left side of) the input field | `string` | - |
36+
| `[nzAddonAfter]` | The label text displayed after (on the right side of) the input field | `string` | - |
37+
| `[nzPrefix]` | The prefix icon for the Input | `string` | - |
38+
| `[nzSuffix]` | The suffix icon for the Input | `string` | - |
39+
| `[nzAllowClear]` | If allow to remove input content with clear icon | `boolean` | `false` |
40+
| `(nzClear)` | Event emitted when the clear icon is clicked | `OutputEmitterRef<void>` | - |
3941

4042
### nz-input-group
4143

components/input/doc/index.zh-CN.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ description: 通过鼠标或键盘输入内容,是最基础的表单域的包
2828

2929
### nz-input-wrapper
3030

31-
当需要为 `[nz-input]` 添加前/后缀或前/后置标签时使用。
32-
33-
| 参数 | 说明 | 类型 | 默认值 |
34-
| ----------------- | ---------------------------- | -------- | ------ |
35-
| `[nzAddonBefore]` | 带标签的 input,设置前置标签 | `string` | - |
36-
| `[nzAddonAfter]` | 带标签的 input,设置后置标签 | `string` | - |
37-
| `[nzPrefix]` | 带有前缀图标的 input | `string` | - |
38-
| `[nzSuffix]` | 带有后缀图标的 input | `string` | - |
31+
当需要为 `[nz-input]` 添加额外功能时使用。
32+
33+
| 参数 | 说明 | 类型 | 默认值 |
34+
| ----------------- | ---------------------------- | ------------------------ | ------- |
35+
| `[nzAddonBefore]` | 带标签的 input,设置前置标签 | `string` | - |
36+
| `[nzAddonAfter]` | 带标签的 input,设置后置标签 | `string` | - |
37+
| `[nzPrefix]` | 带有前缀图标的 input | `string` | - |
38+
| `[nzSuffix]` | 带有后缀图标的 input | `string` | - |
39+
| `[nzAllowClear]` | 可以点击清除图标删除内容 | `boolean` | `false` |
40+
| `(nzClear)` | 点击清除图标时触发 | `OutputEmitterRef<void>` | - |
3941

4042
### nz-input-group
4143

components/input/input-wrapper.component.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { Component, ElementRef, viewChild } from '@angular/core';
77
import { ComponentFixture, TestBed } from '@angular/core/testing';
8+
import { FormsModule } from '@angular/forms';
89

910
import { NzSizeLDSType, NzVariant } from 'ng-zorro-antd/core/types';
1011

@@ -132,6 +133,86 @@ describe('input-wrapper', () => {
132133
});
133134
});
134135

136+
describe('input-wrapper allow clear', () => {
137+
let component: InputAllowClearTestComponent;
138+
let fixture: ComponentFixture<InputAllowClearTestComponent>;
139+
let clearIconElement: HTMLElement;
140+
141+
beforeEach(() => {
142+
fixture = TestBed.createComponent(InputAllowClearTestComponent);
143+
component = fixture.componentInstance;
144+
fixture.autoDetectChanges();
145+
clearIconElement = fixture.nativeElement.querySelector('.ant-input-clear-icon');
146+
});
147+
148+
it('should be show clear icon when input has value', async () => {
149+
expect(clearIconElement.classList).toContain('ant-input-clear-icon-hidden');
150+
component.value = 'test';
151+
fixture.detectChanges();
152+
await fixture.whenStable();
153+
expect(clearIconElement.classList).not.toContain('ant-input-clear-icon-hidden');
154+
component.value = '';
155+
fixture.detectChanges();
156+
await fixture.whenStable();
157+
expect(clearIconElement.classList).toContain('ant-input-clear-icon-hidden');
158+
});
159+
160+
it('should be clear input value when click clear icon', () => {
161+
component.value = 'test';
162+
fixture.detectChanges();
163+
clearIconElement.click();
164+
fixture.detectChanges();
165+
expect(component.value).toBe('');
166+
expect(clearIconElement.classList).toContain('ant-input-clear-icon-hidden');
167+
});
168+
169+
it('should be not show clear icon when input is disabled or readonly', () => {
170+
component.value = 'test';
171+
component.disabled = true;
172+
component.readonly = false;
173+
fixture.detectChanges();
174+
expect(clearIconElement.classList).toContain('ant-input-clear-icon-hidden');
175+
component.disabled = false;
176+
component.readonly = true;
177+
fixture.detectChanges();
178+
expect(clearIconElement.classList).toContain('ant-input-clear-icon-hidden');
179+
});
180+
181+
it('should be not show clear icon when nzAllowClear is false', () => {
182+
component.value = 'test';
183+
component.allowClear = false;
184+
fixture.detectChanges();
185+
expect(fixture.nativeElement.querySelector('.ant-input-clear-icon')).toBeFalsy();
186+
});
187+
188+
it('should be emit nzClear event when click clear icon', () => {
189+
spyOn(component, 'onClear');
190+
component.value = 'test';
191+
fixture.detectChanges();
192+
expect(component.onClear).not.toHaveBeenCalled();
193+
clearIconElement.click();
194+
fixture.detectChanges();
195+
expect(component.onClear).toHaveBeenCalled();
196+
});
197+
});
198+
199+
@Component({
200+
imports: [NzInputModule, FormsModule],
201+
template: `
202+
<nz-input-wrapper [nzAllowClear]="allowClear" (nzClear)="onClear()">
203+
<input nz-input [(ngModel)]="value" [disabled]="disabled" [readonly]="readonly" />
204+
</nz-input-wrapper>
205+
`
206+
})
207+
class InputAllowClearTestComponent {
208+
allowClear = true;
209+
disabled = false;
210+
readonly = false;
211+
value = '';
212+
213+
onClear(): void {}
214+
}
215+
135216
@Component({
136217
imports: [NzInputModule],
137218
template: `

components/input/input-wrapper.component.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Directionality } from '@angular/cdk/bidi';
88
import { NgTemplateOutlet } from '@angular/common';
99
import {
1010
afterNextRender,
11+
booleanAttribute,
1112
ChangeDetectionStrategy,
1213
Component,
1314
computed,
@@ -17,6 +18,7 @@ import {
1718
forwardRef,
1819
inject,
1920
input,
21+
output,
2022
signal,
2123
ViewEncapsulation
2224
} from '@angular/core';
@@ -82,6 +84,20 @@ import { NZ_INPUT_WRAPPER } from './tokens';
8284
<ng-template [ngTemplateOutlet]="input" />
8385
@if (hasSuffix()) {
8486
<span class="ant-input-suffix">
87+
@if (nzAllowClear()) {
88+
<span
89+
class="ant-input-clear-icon"
90+
[class.ant-input-clear-icon-has-suffix]="nzSuffix() || suffix() || hasFeedback()"
91+
[class.ant-input-clear-icon-hidden]="!inputDir().value() || disabled() || readOnly()"
92+
role="button"
93+
tabindex="-1"
94+
(click)="clear()"
95+
>
96+
<ng-content select="[nzInputClearIcon]">
97+
<nz-icon nzType="close-circle" nzTheme="fill" />
98+
</ng-content>
99+
</span>
100+
}
85101
<ng-content select="[nzInputSuffix]">{{ nzSuffix() }}</ng-content>
86102
@if (hasFeedback() && status()) {
87103
<nz-form-item-feedback-icon [status]="status()" />
@@ -103,25 +119,29 @@ import { NZ_INPUT_WRAPPER } from './tokens';
103119
hostDirectives: [NzSpaceCompactItemDirective],
104120
host: {
105121
'[class]': 'class()',
106-
'[class.ant-input-disabled]': 'disabled()'
122+
'[class.ant-input-disabled]': 'disabled()',
123+
'[class.ant-input-affix-wrapper-textarea-with-clear-btn]': 'nzAllowClear() && isTextarea()'
107124
}
108125
})
109126
export class NzInputWrapperComponent {
110127
private readonly focusMonitor = inject(FocusMonitor);
111128

112-
private readonly inputDir = contentChild.required(NzInputDirective);
113-
private readonly inputRef = contentChild.required(NzInputDirective, { read: ElementRef });
129+
protected readonly inputRef = contentChild.required(NzInputDirective, { read: ElementRef });
130+
protected readonly inputDir = contentChild.required(NzInputDirective);
114131

115132
protected readonly prefix = contentChild(NzInputPrefixDirective);
116133
protected readonly suffix = contentChild(NzInputSuffixDirective);
117134
protected readonly addonBefore = contentChild(NzInputAddonBeforeDirective);
118135
protected readonly addonAfter = contentChild(NzInputAddonAfterDirective);
119136

137+
readonly nzAllowClear = input(false, { transform: booleanAttribute });
120138
readonly nzPrefix = input<string>();
121139
readonly nzSuffix = input<string>();
122140
readonly nzAddonBefore = input<string>();
123141
readonly nzAddonAfter = input<string>();
124142

143+
readonly nzClear = output<void>();
144+
125145
readonly size = computed(() => this.inputDir().nzSize());
126146
readonly variant = computed(() => this.inputDir().nzVariant());
127147
readonly disabled = computed(() => this.inputDir().finalDisabled());
@@ -130,7 +150,9 @@ export class NzInputWrapperComponent {
130150
readonly hasFeedback = computed(() => this.inputDir().hasFeedback());
131151

132152
protected readonly hasPrefix = computed(() => !!this.nzPrefix() || !!this.prefix());
133-
protected readonly hasSuffix = computed(() => !!this.nzSuffix() || !!this.suffix() || this.hasFeedback());
153+
protected readonly hasSuffix = computed(
154+
() => !!this.nzSuffix() || !!this.suffix() || this.nzAllowClear() || this.hasFeedback()
155+
);
134156
protected readonly hasAffix = computed(() => this.hasPrefix() || this.hasSuffix());
135157
protected readonly hasAddonBefore = computed(() => !!this.nzAddonBefore() || !!this.addonBefore());
136158
protected readonly hasAddonAfter = computed(() => !!this.nzAddonAfter() || !!this.addonAfter());
@@ -139,6 +161,7 @@ export class NzInputWrapperComponent {
139161
private readonly compactSize = inject(NZ_SPACE_COMPACT_SIZE, { optional: true });
140162
protected readonly dir = inject(Directionality).valueSignal;
141163
protected readonly focused = signal(false);
164+
protected readonly isTextarea = computed(() => this.inputRef().nativeElement instanceof HTMLTextAreaElement);
142165

143166
protected readonly finalSize = computed(() => {
144167
if (this.compactSize) {
@@ -197,4 +220,9 @@ export class NzInputWrapperComponent {
197220
});
198221
});
199222
}
223+
224+
clear(): void {
225+
this.inputDir().ngControl?.control?.setValue('');
226+
this.nzClear.emit();
227+
}
200228
}

components/input/input.directive.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class NzInputDirective implements OnInit {
6666
protected hostView = inject(ViewContainerRef);
6767

6868
readonly ngControl = inject(NgControl, { self: true, optional: true });
69+
readonly value = signal<string>(this.elementRef.nativeElement.value);
6970

7071
/**
7172
* @deprecated Will be removed in v21. It is recommended to use `nzVariant` instead.
@@ -128,6 +129,10 @@ export class NzInputDirective implements OnInit {
128129
this.ngControl?.statusChanges?.pipe(startWith(null), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
129130
this.controlDisabled.set(!!this.ngControl!.disabled);
130131
});
132+
133+
this.ngControl?.valueChanges?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
134+
this.value.set(value);
135+
});
131136
}
132137

133138
private renderFeedbackIcon(): void {

0 commit comments

Comments
 (0)