Skip to content

Commit 800b6cf

Browse files
authored
feat(module:form): support nzRequiredMark (#9447)
1 parent 4d5ec65 commit 800b6cf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+648
-26
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 16
3+
title:
4+
zh-CN: 必选样式
5+
en-US: Required style
6+
---
7+
8+
## zh-CN
9+
10+
通过 `nzRequiredMark` 切换必选与可选样式。
11+
12+
## en-US
13+
14+
Switch required or optional style with `nzRequiredMark`.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NgTemplateOutlet } from '@angular/common';
2+
import { Component, signal } from '@angular/core';
3+
import { FormsModule } from '@angular/forms';
4+
5+
import { NzFormModule, type NzRequiredMark } from 'ng-zorro-antd/form';
6+
import { NzInputModule } from 'ng-zorro-antd/input';
7+
import { NzRadioModule } from 'ng-zorro-antd/radio';
8+
import { NzTagModule } from 'ng-zorro-antd/tag';
9+
10+
@Component({
11+
selector: 'nz-demo-form-required-style',
12+
imports: [FormsModule, NzFormModule, NzRadioModule, NzInputModule, NgTemplateOutlet, NzTagModule],
13+
template: `
14+
<form nz-form [nzRequiredMark]="requiredMarkStyle()">
15+
<nz-radio-group [(ngModel)]="requiredMarkStyle" name="requiredMarkStyle">
16+
<label nz-radio-button [nzValue]="true">Default</label>
17+
<label nz-radio-button nzValue="optional">Optional</label>
18+
<label nz-radio-button [nzValue]="false">Hidden</label>
19+
<label nz-radio-button [nzValue]="customRequiredMark">Custom</label>
20+
</nz-radio-group>
21+
<nz-form-item>
22+
<nz-form-label nzFor="fieldA" nzRequired>Field A</nz-form-label>
23+
<nz-form-control>
24+
<input type="text" nz-input id="fieldA" />
25+
</nz-form-control>
26+
</nz-form-item>
27+
<nz-form-item>
28+
<nz-form-label nzFor="fieldB">Field B</nz-form-label>
29+
<nz-form-control>
30+
<input type="text" nz-input id="fieldB" />
31+
</nz-form-control>
32+
</nz-form-item>
33+
</form>
34+
35+
<ng-template #customRequiredMark let-label let-required="required">
36+
@if (required) {
37+
<nz-tag nzColor="red">Required</nz-tag>
38+
} @else {
39+
<nz-tag nzColor="orange">Optional</nz-tag>
40+
}
41+
<ng-container *ngTemplateOutlet="label" />
42+
</ng-template>
43+
`,
44+
styles: `
45+
nz-radio-group {
46+
margin-bottom: 16px;
47+
}
48+
[nz-form] {
49+
max-width: 600px;
50+
}
51+
`
52+
})
53+
export class NzDemoFormRequiredStyleComponent {
54+
readonly requiredMarkStyle = signal<NzRequiredMark>('optional');
55+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ A form consists of one or more form fields whose type includes input, textarea,
5959
| `[nzTooltipIcon]` | Set default props `[nzTooltipIcon]` value of `nz-form-label` | `string \| { type: string; theme: ThemeType }` | `{ type: 'question-circle', theme: 'outline' }` ||
6060
| `[nzLabelAlign]` | Set default props `[nzLabelAlign]` value of `nz-form-label` | `'left' \| 'right'` | `'right'` |
6161
| `[nzLabelWrap]` | Set default props `[nzLabelWrap]` value of `nz-form-label` | `boolean` | `false` |
62+
| `[nzRequiredMark]` | Required mark style. Can use required mark or optional mark. | `NzRequiredMark` | `true` |
6263

6364
### nz-form-item
6465

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ description: 高性能表单控件,自带数据域管理。包含数据录入
6060
| `[nzTooltipIcon]` | 配置 `nz-form-label``[nzTooltipIcon]` 的默认值 | `string \| { type: string; theme: ThemeType }` | `{ type: 'question-circle', theme: 'outline' }` ||
6161
| `[nzLabelAlign]` | 配置 `nz-form-label``[nzLabelAlign]` 的默认值 | `'left' \| 'right'` | `'right'` |
6262
| `[nzLabelWrap]` | 配置 `nz-form-label``[nzLabelWrap]` 的默认值 | `boolean` | `false` |
63+
| `[nzRequiredMark]` | 必填标记样式。可使用必填标记或可选标记。 | `NzRequiredMark` | `true` |
6364

6465
### nz-form-item
6566

components/form/form-label.component.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
44
*/
55

6+
import { NgTemplateOutlet } from '@angular/common';
67
import {
78
ChangeDetectionStrategy,
9+
ChangeDetectorRef,
810
Component,
911
Input,
1012
ViewEncapsulation,
1113
booleanAttribute,
12-
inject,
13-
ChangeDetectorRef
14+
computed,
15+
inject
1416
} from '@angular/core';
1517
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1618
import { filter } from 'rxjs/operators';
@@ -19,6 +21,8 @@ import { ThemeType } from '@ant-design/icons-angular';
1921

2022
import { NzOutletModule } from 'ng-zorro-antd/core/outlet';
2123
import { NzTSType } from 'ng-zorro-antd/core/types';
24+
import { isTemplateRef } from 'ng-zorro-antd/core/util';
25+
import { NzI18nModule } from 'ng-zorro-antd/i18n';
2226
import { NzIconModule } from 'ng-zorro-antd/icon';
2327
import { NzTooltipDirective } from 'ng-zorro-antd/tooltip';
2428

@@ -40,14 +44,33 @@ function toTooltipIcon(value: string | NzFormTooltipIcon): Required<NzFormToolti
4044
encapsulation: ViewEncapsulation.None,
4145
changeDetection: ChangeDetectionStrategy.OnPush,
4246
template: `
43-
<label [attr.for]="nzFor" [class.ant-form-item-no-colon]="nzNoColon" [class.ant-form-item-required]="nzRequired">
44-
<ng-content></ng-content>
45-
@if (nzTooltipTitle) {
46-
<span class="ant-form-item-tooltip" nz-tooltip [nzTooltipTitle]="nzTooltipTitle">
47-
<ng-container *nzStringTemplateOutlet="tooltipIcon.type; let tooltipIconType">
48-
<nz-icon [nzType]="tooltipIconType" [nzTheme]="tooltipIcon.theme" />
49-
</ng-container>
50-
</span>
47+
<label
48+
[attr.for]="nzFor"
49+
[class.ant-form-item-no-colon]="nzNoColon"
50+
[class.ant-form-item-required]="nzRequired"
51+
[class.ant-form-item-required-mark-optional]="nzRequiredMark?.() === 'optional' || isNzRequiredMarkTemplate()"
52+
[class.ant-form-item-required-mark-hidden]="nzRequiredMark?.() === false"
53+
>
54+
<ng-template #labelTemplate>
55+
<ng-content />
56+
@if (nzTooltipTitle) {
57+
<span class="ant-form-item-tooltip" nz-tooltip [nzTooltipTitle]="nzTooltipTitle">
58+
<ng-container *nzStringTemplateOutlet="tooltipIcon.type; let tooltipIconType">
59+
<nz-icon [nzType]="tooltipIconType" [nzTheme]="tooltipIcon.theme" />
60+
</ng-container>
61+
</span>
62+
}
63+
@if (nzRequiredMark?.() === 'optional' && !nzRequired) {
64+
<span class="ant-form-item-optional">{{ 'Form.optional' | nzI18n }}</span>
65+
}
66+
</ng-template>
67+
68+
@if (isNzRequiredMarkTemplate()) {
69+
<ng-container
70+
*ngTemplateOutlet="$any(nzRequiredMark!()); context: { required: nzRequired, $implicit: labelTemplate }"
71+
/>
72+
} @else {
73+
<ng-container *ngTemplateOutlet="labelTemplate" />
5174
}
5275
</label>
5376
`,
@@ -56,7 +79,7 @@ function toTooltipIcon(value: string | NzFormTooltipIcon): Required<NzFormToolti
5679
'[class.ant-form-item-label-left]': `nzLabelAlign === 'left'`,
5780
'[class.ant-form-item-label-wrap]': `nzLabelWrap`
5881
},
59-
imports: [NzOutletModule, NzTooltipDirective, NzIconModule]
82+
imports: [NzOutletModule, NzTooltipDirective, NzIconModule, NgTemplateOutlet, NzI18nModule]
6083
})
6184
export class NzFormLabelComponent {
6285
private cdr = inject(ChangeDetectorRef);
@@ -110,6 +133,10 @@ export class NzFormLabelComponent {
110133

111134
private nzFormDirective = inject(NzFormDirective, { skipSelf: true, optional: true });
112135

136+
protected readonly nzRequiredMark = this.nzFormDirective?.nzRequiredMark;
137+
138+
protected readonly isNzRequiredMarkTemplate = computed(() => isTemplateRef(this.nzRequiredMark?.()));
139+
113140
constructor() {
114141
if (this.nzFormDirective) {
115142
this.nzFormDirective

components/form/form-label.spec.ts

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
44
*/
55

6-
import { Component, DebugElement } from '@angular/core';
6+
import { NgTemplateOutlet } from '@angular/common';
7+
import { AfterViewInit, Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
78
import { ComponentFixture, TestBed } from '@angular/core/testing';
89
import { By } from '@angular/platform-browser';
910
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
1011

1112
import { NzLabelAlignType } from 'ng-zorro-antd/form/form.directive';
1213
import { NzFormModule } from 'ng-zorro-antd/form/form.module';
14+
import { en_US, NzI18nService } from 'ng-zorro-antd/i18n';
1315

1416
import { NzFormLabelComponent, NzFormTooltipIcon } from './form-label.component';
17+
import { NzRequiredMark } from './types';
1518

1619
const testBedOptions = { imports: [NoopAnimationsModule] };
1720

@@ -87,6 +90,136 @@ describe('nz-form-label', () => {
8790
expect(label.nativeElement.classList).toContain('ant-form-item-label-wrap');
8891
});
8992
});
93+
94+
describe('with form required mark integration', () => {
95+
let fixture: ComponentFixture<NzTestFormLabelRequiredMarkComponent>;
96+
let testComponent: NzTestFormLabelRequiredMarkComponent;
97+
let labels: DebugElement[];
98+
let i18nService: NzI18nService;
99+
100+
beforeEach(() => {
101+
TestBed.configureTestingModule(testBedOptions);
102+
fixture = TestBed.createComponent(NzTestFormLabelRequiredMarkComponent);
103+
testComponent = fixture.componentInstance;
104+
i18nService = TestBed.inject(NzI18nService);
105+
i18nService.setLocale(en_US);
106+
fixture.detectChanges();
107+
labels = fixture.debugElement.queryAll(By.directive(NzFormLabelComponent));
108+
});
109+
110+
it('should inherit required mark from form directive when using boolean true', () => {
111+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
112+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
113+
114+
expect(requiredLabel?.nativeElement.querySelector('label').classList).toContain('ant-form-item-required');
115+
expect(requiredLabel?.nativeElement.querySelector('label').classList).not.toContain(
116+
'ant-form-item-required-mark-optional'
117+
);
118+
expect(optionalLabel?.nativeElement.querySelector('label').classList).not.toContain('ant-form-item-required');
119+
});
120+
121+
it('should show optional styling when form nzRequiredMark is false', () => {
122+
testComponent.requiredMark = false;
123+
fixture.detectChanges();
124+
125+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
126+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
127+
128+
expect(requiredLabel?.nativeElement.querySelector('label').classList).toContain('ant-form-item-required');
129+
expect(requiredLabel?.nativeElement.querySelector('label').classList).toContain(
130+
'ant-form-item-required-mark-hidden'
131+
);
132+
expect(optionalLabel?.nativeElement.querySelector('label').classList).not.toContain('ant-form-item-required');
133+
expect(optionalLabel?.nativeElement.querySelector('label').classList).toContain(
134+
'ant-form-item-required-mark-hidden'
135+
);
136+
});
137+
138+
it('should show optional styling when form nzRequiredMark is "optional"', () => {
139+
testComponent.requiredMark = 'optional';
140+
fixture.detectChanges();
141+
142+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
143+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
144+
145+
expect(requiredLabel?.nativeElement.querySelector('label').classList).toContain('ant-form-item-required');
146+
expect(requiredLabel?.nativeElement.querySelector('label').classList).toContain(
147+
'ant-form-item-required-mark-optional'
148+
);
149+
expect(optionalLabel?.nativeElement.querySelector('label').classList).not.toContain('ant-form-item-required');
150+
});
151+
152+
it('should show optional text when nzRequiredMark is "optional" and field is not required', () => {
153+
testComponent.requiredMark = 'optional';
154+
fixture.detectChanges();
155+
156+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
157+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
158+
159+
// Required label should NOT show (optional) text
160+
expect(requiredLabel?.nativeElement.querySelector('.ant-form-item-optional')).toBeNull();
161+
162+
// Optional label should show (optional) text
163+
expect(optionalLabel?.nativeElement.querySelector('.ant-form-item-optional')).toBeTruthy();
164+
expect(optionalLabel?.nativeElement.querySelector('.ant-form-item-optional').textContent?.trim()).toBe(
165+
'(optional)'
166+
);
167+
});
168+
169+
it('should NOT show optional text when nzRequiredMark is false', () => {
170+
testComponent.requiredMark = false;
171+
fixture.detectChanges();
172+
173+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
174+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
175+
176+
expect(requiredLabel?.nativeElement.querySelector('.ant-form-item-optional')).toBeNull();
177+
expect(optionalLabel?.nativeElement.querySelector('.ant-form-item-optional')).toBeNull();
178+
});
179+
180+
it('should NOT show optional text when nzRequiredMark is true', () => {
181+
testComponent.requiredMark = true;
182+
fixture.detectChanges();
183+
184+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
185+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
186+
187+
expect(requiredLabel?.nativeElement.querySelector('.ant-form-item-optional')).toBeNull();
188+
expect(optionalLabel?.nativeElement.querySelector('.ant-form-item-optional')).toBeNull();
189+
});
190+
191+
it('should use custom template when provided', () => {
192+
testComponent.useCustomTemplate = true;
193+
fixture.detectChanges();
194+
195+
const requiredLabel = labels.find(l => l.nativeElement.classList.contains('required-label'));
196+
const optionalLabel = labels.find(l => l.nativeElement.classList.contains('optional-label'));
197+
198+
expect(requiredLabel?.nativeElement.querySelector('.custom-required')).toBeTruthy();
199+
expect(requiredLabel?.nativeElement.querySelector('.custom-required').textContent?.trim()).toBe('REQUIRED');
200+
expect(optionalLabel?.nativeElement.querySelector('.custom-optional')).toBeTruthy();
201+
expect(optionalLabel?.nativeElement.querySelector('.custom-optional').textContent?.trim()).toBe('OPTIONAL');
202+
203+
expect(requiredLabel?.nativeElement.querySelector('.label-content')).toBeTruthy();
204+
expect(optionalLabel?.nativeElement.querySelector('.label-content')).toBeTruthy();
205+
});
206+
207+
it('should handle template context correctly with required and optional labels', () => {
208+
testComponent.useCustomTemplate = true;
209+
fixture.detectChanges();
210+
211+
const requiredLabelElement = fixture.debugElement.query(By.css('.required-label'));
212+
const optionalLabelElement = fixture.debugElement.query(By.css('.optional-label'));
213+
214+
const requiredCustom = requiredLabelElement.nativeElement.querySelector('.custom-required');
215+
const optionalCustom = optionalLabelElement.nativeElement.querySelector('.custom-optional');
216+
217+
expect(requiredCustom).toBeTruthy();
218+
expect(optionalCustom).toBeTruthy();
219+
expect(requiredCustom.textContent?.trim()).toBe('REQUIRED');
220+
expect(optionalCustom.textContent?.trim()).toBe('OPTIONAL');
221+
});
222+
});
90223
});
91224

92225
@Component({
@@ -112,3 +245,43 @@ export class NzTestFormLabelComponent {
112245
align: NzLabelAlignType = 'right';
113246
labelWrap = false;
114247
}
248+
249+
@Component({
250+
imports: [NzFormModule, NgTemplateOutlet],
251+
template: `
252+
<form nz-form [nzRequiredMark]="useCustomTemplate ? customRequiredMarkTemplate : requiredMark">
253+
<nz-form-item>
254+
<nz-form-label class="required-label" nzRequired>
255+
<span class="label-content">Required Field</span>
256+
</nz-form-label>
257+
</nz-form-item>
258+
<nz-form-item>
259+
<nz-form-label class="optional-label">
260+
<span class="label-content">Optional Field</span>
261+
</nz-form-label>
262+
</nz-form-item>
263+
</form>
264+
265+
<ng-template #customRequiredMarkTemplate let-label let-required="required">
266+
@if (required) {
267+
<span class="custom-required">REQUIRED</span>
268+
} @else {
269+
<span class="custom-optional">OPTIONAL</span>
270+
}
271+
<ng-container *ngTemplateOutlet="label" />
272+
</ng-template>
273+
`
274+
})
275+
export class NzTestFormLabelRequiredMarkComponent implements AfterViewInit {
276+
requiredMark: NzRequiredMark = true;
277+
useCustomTemplate = false;
278+
279+
@ViewChild('customRequiredMarkTemplate', { static: true })
280+
customRequiredMarkTemplate!: TemplateRef<{ $implicit: TemplateRef<void>; required: boolean }>;
281+
282+
ngAfterViewInit(): void {
283+
if (this.useCustomTemplate) {
284+
this.requiredMark = this.customRequiredMarkTemplate;
285+
}
286+
}
287+
}

0 commit comments

Comments
 (0)