Skip to content

Commit f80832a

Browse files
feat(module:input): introduce nz-input-password directive (#9460)
1 parent f74fd13 commit f80832a

File tree

12 files changed

+249
-37
lines changed

12 files changed

+249
-37
lines changed

components/icon/icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
EllipsisOutline,
2828
ExclamationCircleFill,
2929
ExclamationCircleOutline,
30+
EyeInvisibleOutline,
3031
EyeOutline,
3132
FileFill,
3233
FileOutline,
@@ -75,6 +76,7 @@ export const NZ_ICONS_USED_BY_ZORRO: IconDefinition[] = [
7576
ExclamationCircleFill,
7677
ExclamationCircleOutline,
7778
EyeOutline,
79+
EyeInvisibleOutline,
7880
FileFill,
7981
FileOutline,
8082
FilterFill,
Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,47 @@
1-
import { Component } from '@angular/core';
1+
import { Component, signal } from '@angular/core';
22
import { FormsModule } from '@angular/forms';
33

4+
import { NzButtonModule } from 'ng-zorro-antd/button';
5+
import { NzFlexModule } from 'ng-zorro-antd/flex';
46
import { NzIconModule } from 'ng-zorro-antd/icon';
57
import { NzInputModule } from 'ng-zorro-antd/input';
68

79
@Component({
810
selector: 'nz-demo-input-password-input',
9-
imports: [FormsModule, NzInputModule, NzIconModule],
11+
imports: [FormsModule, NzInputModule, NzIconModule, NzFlexModule, NzButtonModule],
1012
template: `
11-
<nz-input-wrapper>
12-
<input
13-
nz-input
14-
[type]="passwordVisible ? 'text' : 'password'"
15-
placeholder="input password"
16-
[(ngModel)]="password"
17-
/>
18-
<nz-icon
19-
nzInputSuffix
20-
class="ant-input-password-icon"
21-
[nzType]="passwordVisible ? 'eye-invisible' : 'eye'"
22-
(click)="passwordVisible = !passwordVisible"
23-
/>
24-
</nz-input-wrapper>
13+
<nz-input-password>
14+
<input nz-input placeholder="input password" [(ngModel)]="password" />
15+
</nz-input-password>
2516
<br />
2617
<br />
27-
<nz-input-wrapper>
28-
<input
29-
nz-input
30-
[type]="passwordVisible ? 'text' : 'password'"
31-
placeholder="input password"
32-
[(ngModel)]="password"
33-
disabled
34-
/>
35-
<nz-icon
36-
nzInputSuffix
37-
class="ant-input-password-icon"
38-
[nzType]="passwordVisible ? 'eye-invisible' : 'eye'"
39-
(click)="passwordVisible = !passwordVisible"
40-
/>
41-
</nz-input-wrapper>
18+
<nz-input-password>
19+
<input nz-input placeholder="input password" [(ngModel)]="password" />
20+
<ng-template nzInputPasswordIcon let-visible>
21+
@if (visible) {
22+
<nz-icon nzType="eye" nzTheme="twotone" />
23+
} @else {
24+
<nz-icon nzType="eye-invisible" nzTheme="outline" />
25+
}
26+
</ng-template>
27+
</nz-input-password>
28+
<br />
29+
<br />
30+
<nz-flex nzGap="8px">
31+
<nz-input-password [(nzVisible)]="passwordVisible" [style.flex]="1">
32+
<input nz-input placeholder="input password" [(ngModel)]="password" />
33+
</nz-input-password>
34+
<button nz-button (click)="passwordVisible.set(!passwordVisible())">
35+
{{ passwordVisible() ? 'Hide' : 'Show' }}
36+
</button>
37+
</nz-flex>
38+
<br />
39+
<nz-input-password>
40+
<input nz-input placeholder="input password" [(ngModel)]="password" disabled />
41+
</nz-input-password>
4242
`
4343
})
4444
export class NzDemoInputPasswordInputComponent {
45-
passwordVisible = false;
46-
password?: string;
45+
readonly passwordVisible = signal(false);
46+
readonly password = signal('');
4747
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ Use when you need to add extra functionality to `[nz-input]`.
3939
| `[nzAllowClear]` | If allow to remove input content with clear icon | `boolean` | `false` |
4040
| `(nzClear)` | Event emitted when the clear icon is clicked | `OutputEmitterRef<void>` | - |
4141

42+
### nz-input-password
43+
44+
All properties of `nz-input-wrapper` can be used.
45+
46+
| Property | Description | Type | Default |
47+
| ---------------------- | --------------------------------------------------------- | --------------------------- | ------- |
48+
| `[nzVisibilityToggle]` | Whether to show the toggle button | `boolean` | `true` |
49+
| `[nzVisible]` | Whether the password is visible, supports two-way binding | `boolean` | `false` |
50+
| `(nzVisibleChange)` | Event emitted when the visibility of the password changes | `OutputEmitterRef<boolean>` | - |
51+
4252
### nz-input-group
4353

4454
> ⚠️ `nz-input-group` has been deprecated in `v20.0.0` and will be removed in `v22.0.0`. Please use the `nz-input-wrapper` component instead.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ description: 通过鼠标或键盘输入内容,是最基础的表单域的包
3939
| `[nzAllowClear]` | 可以点击清除图标删除内容 | `boolean` | `false` |
4040
| `(nzClear)` | 点击清除图标时触发 | `OutputEmitterRef<void>` | - |
4141

42+
### nz-input-password
43+
44+
可使用 `nz-input-wrapper` 的所有属性。
45+
46+
| 参数 | 说明 | 类型 | 默认值 |
47+
| ---------------------- | -------------------------- | --------------------------- | ------- |
48+
| `[nzVisibilityToggle]` | 是否显示切换按钮 | `boolean` | `true` |
49+
| `[nzVisible]` | 是否显示密码,支持双向绑定 | `boolean` | `false` |
50+
| `(nzVisibleChange)` | 是否显示密码变更事件 | `OutputEmitterRef<boolean>` | - |
51+
4252
### nz-input-group
4353

4454
> ⚠️ `nz-input-group` 已在 `v20.0.0` 中废弃,将在 `v22.0.0` 中移除,请使用 `nz-input-wrapper` 组件。
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
import { Component } from '@angular/core';
7+
import { ComponentFixture, TestBed } from '@angular/core/testing';
8+
import { FormsModule } from '@angular/forms';
9+
10+
import { NzInputModule } from './input.module';
11+
12+
describe('input-password', () => {
13+
let component: InputPasswordTestComponent;
14+
let fixture: ComponentFixture<InputPasswordTestComponent>;
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(InputPasswordTestComponent);
18+
component = fixture.componentInstance;
19+
fixture.autoDetectChanges();
20+
});
21+
22+
it('should be apply classes', () => {
23+
const passwordElement = fixture.nativeElement.querySelector('nz-input-password');
24+
expect(passwordElement.classList).toContain('ant-input-password');
25+
});
26+
27+
it('should be toggle visible by two-way binding', () => {
28+
const inputElement = fixture.nativeElement.querySelector('input');
29+
expect(inputElement.type).toEqual('password');
30+
component.visible = true;
31+
fixture.detectChanges();
32+
expect(inputElement.type).toEqual('text');
33+
});
34+
35+
it('should be toggle visible by click toggle button', () => {
36+
const inputElement = fixture.nativeElement.querySelector('input');
37+
const toggleElement = fixture.nativeElement.querySelector('.ant-input-password-icon');
38+
expect(inputElement.type).toEqual('password');
39+
expect(component.visible).toBe(false);
40+
toggleElement.click();
41+
fixture.detectChanges();
42+
expect(inputElement.type).toEqual('text');
43+
expect(component.visible).toBe(true);
44+
toggleElement.click();
45+
fixture.detectChanges();
46+
expect(inputElement.type).toEqual('password');
47+
expect(component.visible).toBe(false);
48+
});
49+
50+
it('should be hide toggle', () => {
51+
expect(fixture.nativeElement.querySelector('.ant-input-password-icon')).toBeTruthy();
52+
component.visibilityToggle = false;
53+
fixture.detectChanges();
54+
expect(fixture.nativeElement.querySelector('.ant-input-password-icon')).toBeFalsy();
55+
});
56+
57+
it('should be custom icon', () => {
58+
component.customeIcon = true;
59+
fixture.detectChanges();
60+
expect(fixture.nativeElement.querySelector('.ant-input-password-icon').textContent.trim()).toEqual('show');
61+
component.visible = true;
62+
fixture.detectChanges();
63+
expect(fixture.nativeElement.querySelector('.ant-input-password-icon').textContent.trim()).toEqual('hide');
64+
});
65+
});
66+
67+
@Component({
68+
imports: [NzInputModule, FormsModule],
69+
template: `
70+
<nz-input-password [nzVisibilityToggle]="visibilityToggle" [(nzVisible)]="visible">
71+
<input nz-input [(ngModel)]="value" [disabled]="disabled" [readonly]="readonly" />
72+
@if (customeIcon) {
73+
<ng-template nzInputPasswordIcon let-visible>{{ visible ? 'hide' : 'show' }}</ng-template>
74+
}
75+
</nz-input-password>
76+
`
77+
})
78+
class InputPasswordTestComponent {
79+
visibilityToggle = true;
80+
visible = false;
81+
customeIcon = false;
82+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
import { Directive, input, model } from '@angular/core';
7+
8+
@Directive({
9+
selector: 'nz-input-password'
10+
})
11+
export class NzInputPasswordDirective {
12+
readonly nzVisibilityToggle = input(true);
13+
readonly nzVisible = model(false);
14+
15+
toggleVisible(): void {
16+
this.nzVisible.update(value => !value);
17+
}
18+
}
19+
20+
@Directive({
21+
selector: '[nzInputPasswordIcon]'
22+
})
23+
export class NzInputPasswordIconDirective {
24+
/**
25+
* @internal
26+
*/
27+
static ngTemplateContextGuard(_: NzInputPasswordIconDirective, context: unknown): context is { $implicit: boolean } {
28+
return true;
29+
}
30+
}

components/input/input-wrapper.component.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
input,
2121
output,
2222
signal,
23+
TemplateRef,
2324
ViewEncapsulation
2425
} from '@angular/core';
2526
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -31,11 +32,12 @@ import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDi
3132

3233
import { NzInputAddonAfterDirective, NzInputAddonBeforeDirective } from './input-addon.directive';
3334
import { NzInputPrefixDirective, NzInputSuffixDirective } from './input-affix.directive';
35+
import { NzInputPasswordDirective, NzInputPasswordIconDirective } from './input-password.directive';
3436
import { NzInputDirective } from './input.directive';
3537
import { NZ_INPUT_WRAPPER } from './tokens';
3638

3739
@Component({
38-
selector: 'nz-input-wrapper',
40+
selector: 'nz-input-wrapper,nz-input-password',
3941
exportAs: 'nzInputWrapper',
4042
imports: [NzIconModule, NzFormItemFeedbackIconComponent, NgTemplateOutlet],
4143
template: `
@@ -87,7 +89,9 @@ import { NZ_INPUT_WRAPPER } from './tokens';
8789
@if (nzAllowClear()) {
8890
<span
8991
class="ant-input-clear-icon"
90-
[class.ant-input-clear-icon-has-suffix]="nzSuffix() || suffix() || hasFeedback()"
92+
[class.ant-input-clear-icon-has-suffix]="
93+
nzSuffix() || suffix() || hasFeedback() || inputPasswordDir?.nzVisibilityToggle()
94+
"
9195
[class.ant-input-clear-icon-hidden]="!inputDir().value() || disabled() || readOnly()"
9296
role="button"
9397
tabindex="-1"
@@ -98,6 +102,23 @@ import { NZ_INPUT_WRAPPER } from './tokens';
98102
</ng-content>
99103
</span>
100104
}
105+
@if (inputPasswordDir && inputPasswordDir.nzVisibilityToggle()) {
106+
<span
107+
class="ant-input-password-icon"
108+
role="button"
109+
tabindex="-1"
110+
(click)="inputPasswordDir.toggleVisible()"
111+
>
112+
@if (inputPasswordIconTmpl(); as tmpl) {
113+
<ng-template
114+
[ngTemplateOutlet]="tmpl"
115+
[ngTemplateOutletContext]="{ $implicit: inputPasswordDir.nzVisible() }"
116+
/>
117+
} @else {
118+
<nz-icon [nzType]="inputPasswordDir.nzVisible() ? 'eye' : 'eye-invisible'" nzTheme="outline" />
119+
}
120+
</span>
121+
}
101122
<ng-content select="[nzInputSuffix]">{{ nzSuffix() }}</ng-content>
102123
@if (hasFeedback() && status()) {
103124
<nz-form-item-feedback-icon [status]="status()" />
@@ -120,19 +141,23 @@ import { NZ_INPUT_WRAPPER } from './tokens';
120141
host: {
121142
'[class]': 'class()',
122143
'[class.ant-input-disabled]': 'disabled()',
144+
'[class.ant-input-password]': 'inputPasswordDir',
123145
'[class.ant-input-affix-wrapper-textarea-with-clear-btn]': 'nzAllowClear() && isTextarea()'
124146
}
125147
})
126148
export class NzInputWrapperComponent {
127149
private readonly focusMonitor = inject(FocusMonitor);
128150

151+
protected readonly inputPasswordDir = inject(NzInputPasswordDirective, { self: true, optional: true });
152+
129153
protected readonly inputRef = contentChild.required(NzInputDirective, { read: ElementRef });
130154
protected readonly inputDir = contentChild.required(NzInputDirective);
131155

132156
protected readonly prefix = contentChild(NzInputPrefixDirective);
133157
protected readonly suffix = contentChild(NzInputSuffixDirective);
134158
protected readonly addonBefore = contentChild(NzInputAddonBeforeDirective);
135159
protected readonly addonAfter = contentChild(NzInputAddonAfterDirective);
160+
protected readonly inputPasswordIconTmpl = contentChild(NzInputPasswordIconDirective, { read: TemplateRef });
136161

137162
readonly nzAllowClear = input(false, { transform: booleanAttribute });
138163
readonly nzPrefix = input<string>();
@@ -151,7 +176,7 @@ export class NzInputWrapperComponent {
151176

152177
protected readonly hasPrefix = computed(() => !!this.nzPrefix() || !!this.prefix());
153178
protected readonly hasSuffix = computed(
154-
() => !!this.nzSuffix() || !!this.suffix() || this.nzAllowClear() || this.hasFeedback()
179+
() => !!this.nzSuffix() || !!this.suffix() || this.nzAllowClear() || this.hasFeedback() || this.inputPasswordDir
155180
);
156181
protected readonly hasAffix = computed(() => this.hasPrefix() || this.hasSuffix());
157182
protected readonly hasAddonBefore = computed(() => !!this.nzAddonBefore() || !!this.addonBefore());

components/input/input.directive.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { NzSizeLDSType, NzStatus, NzVariant } from 'ng-zorro-antd/core/types';
3030
import { getStatusClassNames } from 'ng-zorro-antd/core/util';
3131
import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDirective } from 'ng-zorro-antd/space';
3232

33+
import { NzInputPasswordDirective } from './input-password.directive';
3334
import { NZ_INPUT_WRAPPER } from './tokens';
3435

3536
const PREFIX_CLS = 'ant-input';
@@ -39,6 +40,7 @@ const PREFIX_CLS = 'ant-input';
3940
exportAs: 'nzInput',
4041
host: {
4142
class: 'ant-input',
43+
'[attr.type]': 'type()',
4244
'[class]': 'classes()',
4345
'[class.ant-input-disabled]': 'finalDisabled()',
4446
'[class.ant-input-borderless]': `nzVariant() === 'borderless' || (nzVariant() === 'outlined' && nzBorderless())`,
@@ -64,6 +66,7 @@ export class NzInputDirective implements OnInit {
6466
private inputWrapper = inject(NZ_INPUT_WRAPPER, { host: true, optional: true });
6567
private focusMonitor = inject(FocusMonitor);
6668
protected hostView = inject(ViewContainerRef);
69+
protected readonly inputPasswordDir = inject(NzInputPasswordDirective, { host: true, optional: true });
6770

6871
readonly ngControl = inject(NgControl, { self: true, optional: true });
6972
readonly value = signal<string>(this.elementRef.nativeElement.value);
@@ -96,6 +99,12 @@ export class NzInputDirective implements OnInit {
9699
{ initialValue: false }
97100
);
98101
readonly classes = computed(() => getStatusClassNames(PREFIX_CLS, this.status(), this.hasFeedback()));
102+
readonly type = computed(() => {
103+
if (this.inputPasswordDir) {
104+
return this.inputPasswordDir.nzVisible() ? 'text' : 'password';
105+
}
106+
return this.elementRef.nativeElement.getAttribute('type') || 'text';
107+
});
99108

100109
protected readonly focused = signal<boolean>(false);
101110
protected readonly finalSize = computed(() => {

components/input/input.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { NzInputPrefixDirective, NzInputSuffixDirective } from './input-affix.di
1111
import { NzInputGroupSlotComponent } from './input-group-slot.component';
1212
import { NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective } from './input-group.component';
1313
import { NzInputOtpComponent } from './input-otp.component';
14+
import { NzInputPasswordDirective, NzInputPasswordIconDirective } from './input-password.directive';
1415
import { NzInputWrapperComponent } from './input-wrapper.component';
1516
import { NzInputDirective } from './input.directive';
1617
import { NzTextareaCountComponent } from './textarea-count.component';
@@ -20,6 +21,8 @@ import { NzTextareaCountComponent } from './textarea-count.component';
2021
NzTextareaCountComponent,
2122
NzInputDirective,
2223
NzInputWrapperComponent,
24+
NzInputPasswordDirective,
25+
NzInputPasswordIconDirective,
2326
NzInputAddonBeforeDirective,
2427
NzInputAddonAfterDirective,
2528
NzInputPrefixDirective,
@@ -34,6 +37,8 @@ import { NzTextareaCountComponent } from './textarea-count.component';
3437
NzTextareaCountComponent,
3538
NzInputDirective,
3639
NzInputWrapperComponent,
40+
NzInputPasswordDirective,
41+
NzInputPasswordIconDirective,
3742
NzInputAddonBeforeDirective,
3843
NzInputAddonAfterDirective,
3944
NzInputPrefixDirective,

0 commit comments

Comments
 (0)