Skip to content

Commit 3653ecc

Browse files
authored
feat: support custom label name for validation messages (#365)
1 parent 0f7d3ed commit 3653ecc

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed

packages/arcodesign/components/form/__test__/index.spec.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,131 @@ describe('Form input', () => {
264264
});
265265
});
266266
});
267+
268+
269+
describe('Form validate label', () => {
270+
// Scenario 1: Default Scenario (labelName only)
271+
it('should use labelName for display and validation when label is missing', async () => {
272+
const formRef = React.createRef();
273+
render(
274+
<Form ref={formRef}>
275+
<Form.Item labelName="Label Name Only" field="label_name_only" required>
276+
<Input />
277+
</Form.Item>
278+
</Form>,
279+
);
280+
// Check display
281+
expect(screen.getByText('Label Name Only')).toBeInTheDocument();
282+
283+
// Check validation
284+
act(() => {
285+
formRef.current.form.validateFields().catch(() => {});
286+
});
287+
await waitFor(() => {
288+
expect(screen.getByText('Label Name Only 为必填项')).toBeInTheDocument();
289+
});
290+
});
291+
292+
// Scenario 2: ReactNode Customization (label + labelName)
293+
it('should use label for display and labelName for validation when both provided (label is Node)', async () => {
294+
const formRef = React.createRef();
295+
render(
296+
<Form ref={formRef}>
297+
<Form.Item
298+
label={<span>Complex Label</span>}
299+
labelName="Label Name Validation"
300+
field="complex_custom"
301+
required
302+
>
303+
<Input />
304+
</Form.Item>
305+
</Form>,
306+
);
307+
// Check display (should render ReactNode)
308+
expect(screen.getByText('Complex Label')).toBeInTheDocument();
309+
310+
// Check validation (should use labelName)
311+
act(() => {
312+
formRef.current.form.validateFields().catch(() => {});
313+
});
314+
await waitFor(() => {
315+
expect(screen.getByText('Label Name Validation 为必填项')).toBeInTheDocument();
316+
});
317+
});
318+
319+
// Scenario 3a: String Label Only
320+
it('should use string label for display and validation', async () => {
321+
const formRef = React.createRef();
322+
render(
323+
<Form ref={formRef}>
324+
<Form.Item label="String Label" field="string_label" required>
325+
<Input />
326+
</Form.Item>
327+
</Form>,
328+
);
329+
// Check display
330+
expect(screen.getByText('String Label')).toBeInTheDocument();
331+
332+
// Check validation
333+
act(() => {
334+
formRef.current.form.validateFields().catch(() => {});
335+
});
336+
await waitFor(() => {
337+
expect(screen.getByText('String Label 为必填项')).toBeInTheDocument();
338+
});
339+
});
340+
341+
// Scenario 3b: ReactNode Label Only
342+
it('should use ReactNode for display and field for validation when labelName missing', async () => {
343+
const formRef = React.createRef();
344+
render(
345+
<Form ref={formRef}>
346+
<Form.Item label={<span>Node Label</span>} field="node_field" required>
347+
<Input />
348+
</Form.Item>
349+
</Form>,
350+
);
351+
// Check display
352+
expect(screen.getByText('Node Label')).toBeInTheDocument();
353+
354+
// Check validation (fallback to field)
355+
act(() => {
356+
formRef.current.form.validateFields().catch(() => {});
357+
});
358+
await waitFor(() => {
359+
expect(screen.getByText('node_field 为必填项')).toBeInTheDocument();
360+
});
361+
});
362+
363+
// Edge Case: String Label + Label Name
364+
// Rule says: "Default scenario... prioritize labelName... used for Display"
365+
// BUT "ReactNode Customization... Must provide both... label used for rendering"
366+
// Since we treat provided label as "Customization/Override", label (string) should override display, but labelName should override validation?
367+
// Let's verify behavior matches implementation:
368+
// Display: label ?? labelName -> label
369+
// Validation: labelName || label -> labelName
370+
it('should use label for display and labelName for validation if both strings are provided', async () => {
371+
const formRef = React.createRef();
372+
render(
373+
<Form ref={formRef}>
374+
<Form.Item
375+
label="Display String"
376+
labelName="Validation String"
377+
field="mixed_string"
378+
required
379+
>
380+
<Input />
381+
</Form.Item>
382+
</Form>,
383+
);
384+
// Check display
385+
expect(screen.getByText('Display String')).toBeInTheDocument();
386+
// Check validation
387+
act(() => {
388+
formRef.current.form.validateFields().catch(() => {});
389+
});
390+
await waitFor(() => {
391+
expect(screen.getByText('Validation String 为必填项')).toBeInTheDocument();
392+
});
393+
});
394+
});

packages/arcodesign/components/form/form-item.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class FormItemInner extends PureComponent<IFormItemInnerProps, IFormItemInnerSta
116116
validateField = (validateTrigger?: string): Promise<IFieldError> => {
117117
const { validateMessages } = this.context;
118118
const { getFieldValue } = this.context.form;
119-
const { field, rules, onValidateStatusChange } = this.props;
119+
const { field, rules, onValidateStatusChange, labelName } = this.props;
120120
const value = getFieldValue(field);
121121
// rules: if validateTrigger is not defined, all rules will be validated
122122
// if validateTrigger is defined, only rules with validateTrigger or without rule.validateTrigger will be validated
@@ -131,13 +131,17 @@ class FormItemInner extends PureComponent<IFormItemInnerProps, IFormItemInnerSta
131131

132132
if (curRules?.length && field) {
133133
const fieldDom = this.props.getFormItemRef();
134-
const fieldValidator = new Validator({ [field]: curRules }, { validateMessages });
134+
const validateFieldName = labelName || field;
135+
const fieldValidator = new Validator(
136+
{ [validateFieldName]: curRules },
137+
{ validateMessages },
138+
);
135139
return new ES6Promise<IFieldError>(resolve => {
136140
fieldValidator.validate(
137-
{ [field]: value },
141+
{ [validateFieldName]: value },
138142
(errorsMap: Record<string, ValidatorError[]>) => {
139143
const { errors, warnings, errorTypes } = getErrorAndWarnings(
140-
errorsMap?.[field] || [],
144+
errorsMap?.[validateFieldName] || [],
141145
);
142146
this._errors = errors;
143147
onValidateStatusChange({
@@ -284,6 +288,7 @@ export default forwardRef((props: FormItemProps, ref: Ref<FormItemRef>) => {
284288
requiredIcon,
285289
rules,
286290
className = '',
291+
labelName,
287292
...rest
288293
} = props;
289294
const { prefixCls } = useContext(GlobalContext);
@@ -292,6 +297,10 @@ export default forwardRef((props: FormItemProps, ref: Ref<FormItemRef>) => {
292297
const [errorTypes, setErrorTypes] = useState<string | null>(null);
293298
const [warnings, setWarnings] = useState<ReactNode[]>([]);
294299
const formItemRef = useRef<HTMLDivElement | null>(null);
300+
// Prioritize user provided label (if exists and valid string).
301+
// Only use labelName as substitute when label is invalid (not a string) or not provided.
302+
const labelStr = labelName || (typeof label === 'string' ? label : undefined);
303+
const renderedLabel = label !== undefined ? label : labelName;
295304

296305
const onValidateStatusChange = (validateResult: {
297306
errors: ReactNode[];
@@ -340,7 +349,7 @@ export default forwardRef((props: FormItemProps, ref: Ref<FormItemRef>) => {
340349
</span>
341350
)
342351
: null}
343-
{label}
352+
{renderedLabel}
344353
</div>
345354
<div className={cls(`${prefixCls}-form-item-control-wrapper`)}>
346355
<div className={cls(`${prefixCls}-form-item-control`)}>
@@ -349,6 +358,7 @@ export default forwardRef((props: FormItemProps, ref: Ref<FormItemRef>) => {
349358
rules={fieldRules}
350359
disabled={fieldDisabled}
351360
field={field}
361+
labelName={labelStr}
352362
onValidateStatusChange={onValidateStatusChange}
353363
getFormItemRef={getFormItemRef}
354364
/>

packages/arcodesign/components/form/type.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,12 @@ export interface FormItemProps {
256256
* 表单项名
257257
* @en The form item name
258258
*/
259-
label: ReactNode;
259+
label?: ReactNode;
260+
/**
261+
* 校验提示中的字段名(当 label 为复杂节点时推荐设置)
262+
* @en The field name in the validation message (recommended when label is a complex node)
263+
*/
264+
labelName?: string;
260265
/**
261266
* 表单项Stylesheet
262267
* @en The form item stylesheet
@@ -345,6 +350,11 @@ export interface IFormItemInnerProps {
345350
* @en Form item field
346351
*/
347352
field: string;
353+
/**
354+
* 校验提示中的字段名
355+
* @en The field name in the validation message
356+
*/
357+
labelName?: string;
348358
/**
349359
* 表单项子节点
350360
* @en Form item children

0 commit comments

Comments
 (0)