Skip to content

Commit

Permalink
feat(Form): support scrollToFirstError (#1460)
Browse files Browse the repository at this point in the history
* feat: support scrollToFirstError

* fix: revert className

* docs: update
  • Loading branch information
liweijie0812 authored Jun 20, 2024
1 parent 41c382c commit 68135e8
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 70 deletions.
18 changes: 6 additions & 12 deletions src/form/__test__/__snapshots__/demo.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -718,10 +718,9 @@ exports[`Form > Form horizontalVue demo works fine 1`] = `
</div>
<svg
class="t-icon t-icon-chevron-right"
color="rgba(0, 0, 0, .4)"
fill="none"
height="1em"
style="font-size: 24px;"
style="font-size: 24px; color: rgba(0, 0, 0, 0.4);"
viewBox="0 0 24 24"
width="1em"
>
Expand Down Expand Up @@ -971,10 +970,9 @@ exports[`Form > Form horizontalVue demo works fine 1`] = `
</div>
<svg
class="t-icon t-icon-chevron-right"
color="rgba(0, 0, 0, .4)"
fill="none"
height="1em"
style="font-size: 24px;"
style="font-size: 24px; color: rgba(0, 0, 0, 0.4);"
viewBox="0 0 24 24"
width="1em"
>
Expand Down Expand Up @@ -2407,10 +2405,9 @@ exports[`Form > Form mobileVue demo works fine 1`] = `
</div>
<svg
class="t-icon t-icon-chevron-right"
color="rgba(0, 0, 0, .4)"
fill="none"
height="1em"
style="font-size: 24px;"
style="font-size: 24px; color: rgba(0, 0, 0, 0.4);"
viewBox="0 0 24 24"
width="1em"
>
Expand Down Expand Up @@ -2660,10 +2657,9 @@ exports[`Form > Form mobileVue demo works fine 1`] = `
</div>
<svg
class="t-icon t-icon-chevron-right"
color="rgba(0, 0, 0, .4)"
fill="none"
height="1em"
style="font-size: 24px;"
style="font-size: 24px; color: rgba(0, 0, 0, 0.4);"
viewBox="0 0 24 24"
width="1em"
>
Expand Down Expand Up @@ -3959,10 +3955,9 @@ exports[`Form > Form verticalVue demo works fine 1`] = `
</div>
<svg
class="t-icon t-icon-chevron-right"
color="rgba(0, 0, 0, .4)"
fill="none"
height="1em"
style="font-size: 24px;"
style="font-size: 24px; color: rgba(0, 0, 0, 0.4);"
viewBox="0 0 24 24"
width="1em"
>
Expand Down Expand Up @@ -4210,10 +4205,9 @@ exports[`Form > Form verticalVue demo works fine 1`] = `
</div>
<svg
class="t-icon t-icon-chevron-right"
color="rgba(0, 0, 0, .4)"
fill="none"
height="1em"
style="font-size: 24px;"
style="font-size: 24px; color: rgba(0, 0, 0, 0.4);"
viewBox="0 0 24 24"
width="1em"
>
Expand Down
1 change: 1 addition & 0 deletions src/form/demos/horizontal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
show-error-message
label-align="left"
:disabled="disabled"
scroll-to-first-error="auto"
@reset="onReset"
@submit="onSubmit"
>
Expand Down
1 change: 1 addition & 0 deletions src/form/demos/vertical.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
show-error-message
label-align="top"
:disabled="disabled"
scroll-to-first-error="auto"
@reset="onReset"
@submit="onSubmit"
>
Expand Down
58 changes: 29 additions & 29 deletions src/form/form-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
ref,
toRefs,
watch,
getCurrentInstance,
h,
} from 'vue';
import isArray from 'lodash/isArray';
import isNumber from 'lodash/isNumber';
Expand All @@ -22,7 +20,6 @@ import lodashSet from 'lodash/set';
import isNil from 'lodash/isNil';
import lodashTemplate from 'lodash/template';
import { ChevronRightIcon } from 'tdesign-icons-vue-next';
import { renderTNode, TNode } from '../shared';
import { validate } from './form-model';

import {
Expand All @@ -40,25 +37,24 @@ import {
ErrorListType,
FormInjectionKey,
FormItemContext,
FormItemInjectionKey,
SuccessListType,
ValidateStatus,
} from './const';
import config from '../config';
import { useTNodeJSX } from '../hooks/tnode';
import { usePrefixClass } from '../hooks/useClass';

const { prefix } = config;
const name = `${prefix}-form-item`;
const classPrefix = `${prefix}-form__item`;

export type FormItemValidateResult<T extends Data = Data> = { [key in keyof T]: boolean | AllValidateResult[] };

export default defineComponent({
name,
components: { TNode },
name: `${prefix}-form-item`,
props,
setup(props, { slots }) {
const renderTNodeJSX = useTNodeJSX();
const formClass = usePrefixClass('form');
const formItemClass = usePrefixClass('form__item');
const { name } = toRefs(props);

const form = inject(FormInjectionKey, undefined);
Expand All @@ -74,11 +70,11 @@ export default defineComponent({
return null;
});

const formItemClass = computed(() => [
`${prefix}-form__item`,
`${prefix}-form__item--bordered`,
`${prefix}-form--${labelAlign.value}`,
`${prefix}-form-item__${props.name}`,
const formItemClasses = computed(() => [
formItemClass.value,
`${formItemClass.value}--bordered`,
`${formClass.value}--${labelAlign.value}`,
`${formClass.value}-item__${props.name}`,
]);

const needRequiredMark = computed(() => {
Expand All @@ -89,19 +85,19 @@ export default defineComponent({

const hasLabel = computed(() => slots.label || props.label);
const hasColon = computed(() => !!(form?.colon && hasLabel.value));
const FROM_LABEL = `${prefix}-form__label`;
const labelClass = `${formClass.value}__label`;
const labelAlign = computed(() => (isNil(props.labelAlign) ? form?.labelAlign : props.labelAlign));
const labelWidth = computed(() => (isNil(props.labelWidth) ? form?.labelWidth : props.labelWidth));
const contentAlign = computed(() => (isNil(props.contentAlign) ? form?.contentAlign : props.contentAlign));

const labelClasses = computed(() => [
`${prefix}-form__label`,
labelClass,
{
[`${FROM_LABEL}--required`]: needRequiredMark.value,
[`${FROM_LABEL}--colon`]: hasColon.value,
[`${FROM_LABEL}--top`]: hasLabel.value && (labelAlign.value === 'top' || !labelWidth.value),
[`${FROM_LABEL}--left`]: labelAlign.value === 'left' && labelWidth.value,
[`${FROM_LABEL}--right`]: labelAlign.value === 'right' && labelWidth.value,
[`${labelClass}--required`]: needRequiredMark.value,
[`${labelClass}--colon`]: hasColon.value,
[`${labelClass}--top`]: hasLabel.value && (labelAlign.value === 'top' || !labelWidth.value),
[`${labelClass}--left`]: labelAlign.value === 'left' && labelWidth.value,
[`${labelClass}--right`]: labelAlign.value === 'right' && labelWidth.value,
},
]);

Expand All @@ -123,13 +119,13 @@ export default defineComponent({
if (!showErrorMessage.value) return '';
if (!errorList.value.length) return '';
const type = errorList.value[0].type || 'error';
return type === 'error' ? `${classPrefix}--error` : `${classPrefix}--warning`;
return type === 'error' ? `${formItemClass.value}--error` : `${formItemClass.value}--warning`;
});

const contentClasses = computed(() => [`${prefix}-form__controls`, errorClasses.value]);
const contentClasses = computed(() => [`${formClass.value}__controls`, errorClasses.value]);
const contentSlotClasses = computed(() => [
`${prefix}-form__controls-content`,
`${prefix}-form__controls--${contentAlign.value}`,
`${formClass.value}__controls-content`,
`${formClass.value}__controls--${contentAlign.value}`,
]);

const contentStyle = computed(() => {
Expand Down Expand Up @@ -321,7 +317,7 @@ export default defineComponent({
if (!props.arrow) {
return null;
}
return h(ChevronRightIcon, { size: '24px', color: 'rgba(0, 0, 0, .4)' });
return <ChevronRightIcon size="24px" style={{ color: 'rgba(0, 0, 0, .4)' }} />;
};
const renderLabelContent = () => {
if (Number(labelWidth.value) === 0) {
Expand All @@ -334,22 +330,26 @@ export default defineComponent({
if (!helpNode) {
return null;
}
return <div class={[`${classPrefix}-help`, `${prefix}-form__controls--${contentAlign.value}`]}>{helpNode}</div>;
return (
<div class={[`${formItemClass.value}-help`, `${formClass.value}__controls--${contentAlign.value}`]}>
{helpNode}
</div>
);
};
const renderExtraNode = () => {
if (!extraNode.value) {
return null;
}
return (
<div class={[`${classPrefix}-extra`, `${prefix}-form__controls--${contentAlign.value}`]}>
<div class={[`${formItemClass.value}-extra`, `${formClass.value}__controls--${contentAlign.value}`]}>
{extraNode.value}
</div>
);
};

return (
<div class={[...formItemClass.value, renderHelpNode() ? `${prefix}-form__item-with-help` : '']}>
<div class={[`${classPrefix}-wrap`, `${classPrefix}--${labelAlign.value}`]}>
<div class={[...formItemClasses.value, renderHelpNode() ? `${formClass.value}__item-with-help` : '']}>
<div class={[`${formItemClass.value}-wrap`, `${formItemClass.value}--${labelAlign.value}`]}>
<div class={labelClasses.value} style={labelStyle.value}>
<label for={props.for}>{renderLabelContent()}</label>
</div>
Expand Down
22 changes: 9 additions & 13 deletions src/form/form.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@
name | type | default | description | required
-- | -- | -- | -- | --
colon | Boolean | false | \- | N
contentAlign | String | left | options: left/right | N
data | Object | {} | Typescript:`FormData` | N
disabled | Boolean | undefined | \- | N
errorMessage | Object | - | Typescript:`FormErrorMessage` | N
formControlledComponents | Array | - | Typescript:`Array<string>` | N
labelAlign | String | right | optionsleft/right/top | N
labelAlign | String | right | options: left/right/top | N
labelWidth | String / Number | '81px' | \- | N
layout | String | vertical | options:vertical/inline | N
preventSubmitDefault | Boolean | true | \- | N
requiredMark | Boolean | undefined | \- | N
resetType | String | empty | optionsempty/initial | N
resetType | String | empty | options: empty/initial | N
rules | Object | - | Typescript:`FormRules<FormData>` `type FormRules<T extends Data> = { [field in keyof T]?: Array<FormRule> }`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/form/type.ts) | N
scrollToFirstError | String | - | optionssmooth/auto | N
scrollToFirstError | String | - | options: ''/smooth/auto | N
showErrorMessage | Boolean | true | \- | N
statusIcon | Boolean / Slot / Function | undefined | Typescript:`boolean \| TNode<TdFormItemProps>`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
submitWithWarningMessage | Boolean | false | \- | N
onReset | Function | | Typescript:`(context: { e?: FormResetEvent }) => void`<br/> | N
onSubmit | Function | | Typescript:`(context: SubmitContext<FormData>) => void`<br/>[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/form/type.ts)。<br/>`interface SubmitContext<T extends Data = Data> { e?: FormSubmitEvent; validateResult: FormValidateResult<T>; firstError?: string; fields?: any }`<br/><br/>`type FormValidateResult<T> = boolean \| ValidateResultObj<T>`<br/><br/>`type ValidateResultObj<T> = { [key in keyof T]: boolean \| ValidateResultList }`<br/><br/>`type ValidateResultList = Array<AllValidateResult>`<br/><br/>`type AllValidateResult = CustomValidateObj \| ValidateResultType`<br/><br/>`interface ValidateResultType extends FormRule { result: boolean }`<br/><br/>`type ValidateResult<T> = { [key in keyof T]: boolean \| ErrorList }`<br/><br/>`type ErrorList = Array<FormRule>`<br/> | N
Expand All @@ -45,23 +43,21 @@ submit | `(params?: { showErrorMessage?: boolean })` | \- | required
validate | `(params?: FormValidateParams)` | `Promise<FormValidateResult<FormData>>` | required。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/form/type.ts)。<br/>`interface FormValidateParams { fields?: Array<string>; showErrorMessage?: boolean; trigger?: ValidateTriggerType }`<br/><br/>`type ValidateTriggerType = 'blur' \| 'change' \| 'all'`<br/>
validateOnly | `(params?: Pick<FormValidateParams, 'fields' \| 'trigger'>)` | `Promise<FormValidateResult<FormData>>` | required


### FormItem Props

name | type | default | description | required
-- | -- | -- | -- | --
contentAlign | String | left | options: left/right | N
for | String | - | \- | N
help | String / Slot / Function | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
label | String / Slot / Function | '' | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
labelAlign | String | - | optionsleft/right/top | N
labelAlign | String | - | options: left/right/top | N
labelWidth | String / Number | - | \- | N
name | String / Number | - | Typescript:`string \| number` | N
requiredMark | Boolean | undefined | \- | N
rules | Array | - | Typescript:`Array<FormRule>` | N
showErrorMessage | Boolean | undefined | \- | N
status | String | - | Typescript:`'error' \| 'warning' \| 'success' \| 'validating'` | N
statusIcon | Boolean / Slot / Function | undefined | Typescript:`boolean \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
successBorder | Boolean | false | \- | N
tips | String / Slot / Function | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N

### FormRule

Expand All @@ -80,8 +76,8 @@ number | Boolean | - | \- | N
pattern | Object | - | Typescript:`RegExp` | N
required | Boolean | - | \- | N
telnumber | Boolean | - | \- | N
trigger | String | change | optionschange/blur | N
type | String | error | optionserror/warning | N
trigger | String | change | options: change/blur | N
type | String | error | options: error/warning | N
url | Boolean / Object | - | Typescript:`boolean \| IsURLOptions` `import { IsURLOptions } from 'validator/es/lib/isURL'`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/form/type.ts) | N
validator | Function | - | Typescript:`CustomValidator` `type CustomValidator = (val: ValueType) => CustomValidateResolveType \| Promise<CustomValidateResolveType>` `type CustomValidateResolveType = boolean \| CustomValidateObj` `interface CustomValidateObj { result: boolean; message: string; type?: 'error' \| 'warning' \| 'success' }` `type ValueType = any`[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/form/type.ts) | N
whitespace | Boolean | - | \- | N
Expand Down
Loading

0 comments on commit 68135e8

Please sign in to comment.