From 1b6bae671dad6ecd34548a97905ab14afcc08428 Mon Sep 17 00:00:00 2001 From: neverland <chenjiahan@youzan.com> Date: Sun, 14 Nov 2021 16:57:12 +0800 Subject: [PATCH] chore(AddressEdit): using Form component (#9858) --- packages/vant/package.json | 1 + .../vant/src/address-edit/AddressEdit.tsx | 163 +++++++----------- .../src/address-edit/AddressEditDetail.tsx | 14 +- .../test/__snapshots__/demo.spec.ts.snap | 6 +- .../test/__snapshots__/index.spec.js.snap | 110 +----------- .../vant/src/address-edit/test/index.spec.js | 44 ++--- pnpm-lock.yaml | 10 ++ 7 files changed, 107 insertions(+), 241 deletions(-) diff --git a/packages/vant/package.json b/packages/vant/package.json index 840e3ab03..69b7aa5ba 100644 --- a/packages/vant/package.json +++ b/packages/vant/package.json @@ -56,6 +56,7 @@ "@vant/cli": "^4.0.0-beta.5", "@vue/compiler-sfc": "^3.2.20", "@vue/runtime-core": "^3.2.20", + "@vue/test-utils": "^2.0.0-rc.16", "typescript": "4.x", "vue": "^3.2.20", "vue-router": "^4.0.12" diff --git a/packages/vant/src/address-edit/AddressEdit.tsx b/packages/vant/src/address-edit/AddressEdit.tsx index d6dcc3cac..69229bfb2 100644 --- a/packages/vant/src/address-edit/AddressEdit.tsx +++ b/packages/vant/src/address-edit/AddressEdit.tsx @@ -27,7 +27,8 @@ import { useExpose } from '../composables/use-expose'; // Components import { Area, AreaList, AreaColumnOption, AreaInstance } from '../area'; import { Cell } from '../cell'; -import { Field } from '../field'; +import { Form } from '../form'; +import { Field, FieldRule } from '../field'; import { Popup } from '../popup'; import { Toast } from '../toast'; import { Button } from '../button'; @@ -111,25 +112,16 @@ export default defineComponent({ setup(props, { emit, slots }) { const areaRef = ref<AreaInstance>(); - const state = reactive({ - data: {} as AddressEditInfo, - showAreaPopup: false, - detailFocused: false, - errorInfo: { - tel: '', - name: '', - areaCode: '', - postalCode: '', - addressDetail: '', - } as Record<string, string>, - }); + const data = reactive({} as AddressEditInfo); + const showAreaPopup = ref(false); + const detailFocused = ref(false); const areaListLoaded = computed( () => isObject(props.areaList) && Object.keys(props.areaList).length ); const areaText = computed(() => { - const { country, province, city, county, areaCode } = state.data; + const { country, province, city, county, areaCode } = data; if (areaCode) { const arr = [country, province, city, county]; if (province && province === city) { @@ -142,7 +134,7 @@ export default defineComponent({ // hide bottom field when use search && detail get focused const hideBottomFields = computed( - () => props.searchResult?.length && state.detailFocused + () => props.searchResult?.length && detailFocused.value ); const assignAreaValues = () => { @@ -150,70 +142,52 @@ export default defineComponent({ const detail: Record<string, string> = areaRef.value.getArea(); detail.areaCode = detail.code; delete detail.code; - extend(state.data, detail); + extend(data, detail); } }; const onFocus = (key: string) => { - state.errorInfo[key] = ''; - state.detailFocused = key === 'addressDetail'; + detailFocused.value = key === 'addressDetail'; emit('focus', key); }; - const getErrorMessage = (key: string) => { - const value = String((state.data as any)[key] || '').trim(); + const rules = computed<Record<string, FieldRule[]>>(() => { + const { validator, telValidator, postalValidator } = props; - if (props.validator) { - const message = props.validator(key, value); - if (message) { - return message; - } - } - - switch (key) { - case 'name': - return value ? '' : t('nameEmpty'); - case 'tel': - return props.telValidator(value) ? '' : t('telInvalid'); - case 'areaCode': - return value ? '' : t('areaEmpty'); - case 'addressDetail': - return value ? '' : t('addressEmpty'); - case 'postalCode': - return value && !props.postalValidator(value) ? t('postalEmpty') : ''; - } - }; - - const onSave = () => { - const items = ['name', 'tel']; - - if (props.showArea) { - items.push('areaCode'); - } - - if (props.showDetail) { - items.push('addressDetail'); - } - - if (props.showPostal) { - items.push('postalCode'); - } - - const isValid = items.every((item) => { - const msg = getErrorMessage(item); - if (msg) { - state.errorInfo[item] = msg; - } - return !msg; + const makeRule = (name: string, emptyMessage: string): FieldRule => ({ + validator: (value) => { + if (validator) { + const message = validator(name, value); + if (message) { + return message; + } + } + if (!value) { + return emptyMessage; + } + return true; + }, }); - if (isValid && !props.isSaving) { - emit('save', state.data); - } - }; + return { + name: [makeRule('name', t('nameEmpty'))], + tel: [ + makeRule('tel', t('telInvalid')), + { validator: telValidator, message: t('telInvalid') }, + ], + areaCode: [makeRule('areaCode', t('areaEmpty'))], + addressDetail: [makeRule('addressDetail', t('addressEmpty'))], + postalCode: [ + makeRule('addressDetail', t('postalEmpty')), + { validator: postalValidator, message: t('postalEmpty') }, + ], + }; + }); + + const onSave = () => emit('save', data); const onChangeDetail = (val: string) => { - state.data.addressDetail = val; + data.addressDetail = val; emit('change-detail', val); }; @@ -222,22 +196,21 @@ export default defineComponent({ if (values.some((value) => !value.code)) { Toast(t('areaEmpty')); - return; + } else { + showAreaPopup.value = false; + assignAreaValues(); + emit('change-area', values); } - - state.showAreaPopup = false; - assignAreaValues(); - emit('change-area', values); }; - const onDelete = () => emit('delete', state.data); + const onDelete = () => emit('delete', data); // get values of area component - const getArea = () => (areaRef.value ? areaRef.value.getValues() : []); + const getArea = () => areaRef.value?.getValues() || []; // set area code to area component const setAreaCode = (code?: string) => { - state.data.areaCode = code || ''; + data.areaCode = code || ''; if (code) { nextTick(assignAreaValues); @@ -247,12 +220,12 @@ export default defineComponent({ const onDetailBlur = () => { // await for click search event setTimeout(() => { - state.detailFocused = false; + detailFocused.value = false; }); }; const setAddressDetail = (value: string) => { - state.data.addressDetail = value; + data.addressDetail = value; }; const renderSetDefaultCell = () => { @@ -260,7 +233,7 @@ export default defineComponent({ const slots = { 'right-icon': () => ( <Switch - v-model={state.data.isDefault} + v-model={data.isDefault} size="24" onChange={(event) => emit('change-default', event)} /> @@ -277,8 +250,6 @@ export default defineComponent({ /> ); } - - return null; }; useExpose({ @@ -289,13 +260,13 @@ export default defineComponent({ watch( () => props.areaList, - () => setAreaCode(state.data.areaCode) + () => setAreaCode(data.areaCode) ); watch( () => props.addressInfo, (value) => { - state.data = extend({}, DEFAULT_DATA, value); + extend(data, DEFAULT_DATA, value); setAreaCode(value.areaCode); }, { @@ -305,18 +276,17 @@ export default defineComponent({ ); return () => { - const { data, errorInfo } = state; const { disableArea } = props; return ( - <div class={bem()}> + <Form class={bem()} onSubmit={onSave}> <div class={bem('fields')}> <Field v-model={data.name} clearable label={t('name')} + rules={rules.value.name} placeholder={t('name')} - errorMessage={errorInfo.name} onFocus={() => onFocus('name')} /> <Field @@ -324,9 +294,9 @@ export default defineComponent({ clearable type="tel" label={t('tel')} + rules={rules.value.tel} maxlength={props.telMaxlength} placeholder={t('tel')} - errorMessage={errorInfo.tel} onFocus={() => onFocus('tel')} /> <Field @@ -335,22 +305,22 @@ export default defineComponent({ label={t('area')} is-link={!disableArea} modelValue={areaText.value} + rules={rules.value.areaCode} placeholder={props.areaPlaceholder || t('area')} - errorMessage={errorInfo.areaCode} onFocus={() => onFocus('areaCode')} onClick={() => { emit('click-area'); - state.showAreaPopup = !disableArea; + showAreaPopup.value = !disableArea; }} /> <AddressEditDetail show={props.showDetail} + rows={props.detailRows} + rules={rules.value.addressDetail} value={data.addressDetail} - focused={state.detailFocused} - detailRows={props.detailRows} - errorMessage={errorInfo.addressDetail} + focused={detailFocused.value} + maxlength={props.detailMaxlength} searchResult={props.searchResult} - detailMaxlength={props.detailMaxlength} showSearchResult={props.showSearchResult} onBlur={onDetailBlur} onFocus={() => onFocus('addressDetail')} @@ -362,10 +332,10 @@ export default defineComponent({ v-show={!hideBottomFields.value} v-model={data.postalCode} type="tel" + rules={rules.value.postalCode} label={t('postal')} maxlength="6" placeholder={t('postal')} - errorMessage={errorInfo.postalCode} onFocus={() => onFocus('postalCode')} /> )} @@ -380,6 +350,7 @@ export default defineComponent({ text={props.saveButtonText || t('save')} class={bem('button')} loading={props.isSaving} + nativeType="submit" onClick={onSave} /> {props.showDelete && ( @@ -394,7 +365,7 @@ export default defineComponent({ )} </div> <Popup - v-model:show={state.showAreaPopup} + v-model:show={showAreaPopup.value} round teleport="body" position="bottom" @@ -408,11 +379,11 @@ export default defineComponent({ columnsPlaceholder={props.areaColumnsPlaceholder} onConfirm={onAreaConfirm} onCancel={() => { - state.showAreaPopup = false; + showAreaPopup.value = false; }} /> </Popup> - </div> + </Form> ); }; }, diff --git a/packages/vant/src/address-edit/AddressEditDetail.tsx b/packages/vant/src/address-edit/AddressEditDetail.tsx index 3401e5d1b..f048ed0b5 100644 --- a/packages/vant/src/address-edit/AddressEditDetail.tsx +++ b/packages/vant/src/address-edit/AddressEditDetail.tsx @@ -9,7 +9,7 @@ import { Field } from '../field'; // Types import type { AddressEditSearchItem } from './types'; -import type { FieldInstance } from '../field/types'; +import type { FieldRule, FieldInstance } from '../field/types'; const [name, bem, t] = createNamespace('address-edit-detail'); @@ -18,12 +18,12 @@ export default defineComponent({ props: { show: Boolean, + rows: numericProp, value: String, + rules: Array as PropType<FieldRule[]>, focused: Boolean, - detailRows: numericProp, + maxlength: numericProp, searchResult: Array as PropType<AddressEditSearchItem[]>, - errorMessage: String, - detailMaxlength: numericProp, showSearchResult: Boolean, }, @@ -86,14 +86,14 @@ export default defineComponent({ clearable ref={field} class={bem()} - rows={props.detailRows} + rows={props.rows} type="textarea" + rules={props.rules} label={t('label')} border={!showSearchResult()} - maxlength={props.detailMaxlength} + maxlength={props.maxlength} modelValue={props.value} placeholder={t('placeholder')} - errorMessage={props.errorMessage} onBlur={onBlur} onFocus={onFocus} onUpdate:modelValue={onInput} diff --git a/packages/vant/src/address-edit/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/address-edit/test/__snapshots__/demo.spec.ts.snap index 3a44405ce..aabd05ff7 100644 --- a/packages/vant/src/address-edit/test/__snapshots__/demo.spec.ts.snap +++ b/packages/vant/src/address-edit/test/__snapshots__/demo.spec.ts.snap @@ -2,7 +2,7 @@ exports[`should render demo and match snapshot 1`] = ` <div> - <div class="van-address-edit"> + <form class="van-form van-address-edit"> <div class="van-address-edit__fields"> <div class="van-cell van-field"> <div class="van-cell__title van-field__label"> @@ -104,7 +104,7 @@ exports[`should render demo and match snapshot 1`] = ` </div> </div> <div class="van-address-edit__buttons"> - <button type="button" + <button type="submit" class="van-button van-button--danger van-button--normal van-button--block van-button--round van-address-edit__button" > <div class="van-button__content"> @@ -123,6 +123,6 @@ exports[`should render demo and match snapshot 1`] = ` </div> </button> </div> - </div> + </form> </div> `; diff --git a/packages/vant/src/address-edit/test/__snapshots__/index.spec.js.snap b/packages/vant/src/address-edit/test/__snapshots__/index.spec.js.snap index ef5b38f22..279729d12 100644 --- a/packages/vant/src/address-edit/test/__snapshots__/index.spec.js.snap +++ b/packages/vant/src/address-edit/test/__snapshots__/index.spec.js.snap @@ -7,7 +7,7 @@ exports[`should allow to custom validator with validator prop 1`] = ` `; exports[`should render AddressEdit correctly 1`] = ` -<div class="van-address-edit"> +<form class="van-form van-address-edit"> <div class="van-address-edit__fields"> <div class="van-cell van-field"> <div class="van-cell__title van-field__label"> @@ -78,7 +78,7 @@ exports[`should render AddressEdit correctly 1`] = ` </div> </div> <div class="van-address-edit__buttons"> - <button type="button" + <button type="submit" class="van-button van-button--danger van-button--normal van-button--block van-button--round van-address-edit__button" > <div class="van-button__content"> @@ -88,11 +88,11 @@ exports[`should render AddressEdit correctly 1`] = ` </div> </button> </div> -</div> +</form> `; exports[`should render AddressEdit with props correctly 1`] = ` -<div class="van-address-edit"> +<form class="van-form van-address-edit"> <div class="van-address-edit__fields"> <div class="van-cell van-field"> <div class="van-cell__title van-field__label"> @@ -193,7 +193,7 @@ exports[`should render AddressEdit with props correctly 1`] = ` </div> </div> <div class="van-address-edit__buttons"> - <button type="button" + <button type="submit" class="van-button van-button--danger van-button--normal van-button--block van-button--round van-address-edit__button" > <div class="van-button__content"> @@ -203,7 +203,7 @@ exports[`should render AddressEdit with props correctly 1`] = ` </div> </button> </div> -</div> +</form> `; exports[`should valid address detail and render error message correctly 1`] = ` @@ -229,26 +229,6 @@ exports[`should valid address detail and render error message correctly 1`] = ` </div> `; -exports[`should valid address detail and render error message correctly 2`] = ` -<div class="van-cell van-field van-address-edit-detail"> - <div class="van-cell__title van-field__label"> - <label> - Address - </label> - </div> - <div class="van-cell__value van-field__value"> - <div class="van-field__body"> - <textarea rows="1" - class="van-field__control" - placeholder="Address" - style="height: auto;" - > - </textarea> - </div> - </div> -</div> -`; - exports[`should valid area code and render error message correctly 1`] = ` <div class="van-cell van-cell--clickable van-field" role="button" @@ -276,30 +256,6 @@ exports[`should valid area code and render error message correctly 1`] = ` </div> `; -exports[`should valid area code and render error message correctly 2`] = ` -<div class="van-cell van-cell--clickable van-field" - role="button" - tabindex="0" -> - <div class="van-cell__title van-field__label"> - <label> - Area - </label> - </div> - <div class="van-cell__value van-field__value"> - <div class="van-field__body"> - <input type="text" - class="van-field__control" - readonly - placeholder="Area" - > - </div> - </div> - <i class="van-badge__wrapper van-icon van-icon-arrow van-cell__right-icon"> - </i> -</div> -`; - exports[`should valid name and render error message correctly 1`] = ` <div class="van-cell van-field"> <div class="van-cell__title van-field__label"> @@ -321,24 +277,6 @@ exports[`should valid name and render error message correctly 1`] = ` </div> `; -exports[`should valid name and render error message correctly 2`] = ` -<div class="van-cell van-field"> - <div class="van-cell__title van-field__label"> - <label> - Name - </label> - </div> - <div class="van-cell__value van-field__value"> - <div class="van-field__body"> - <input type="text" - class="van-field__control" - placeholder="Name" - > - </div> - </div> -</div> -`; - exports[`should valid postal code and render error message correctly 1`] = ` <div class="van-cell van-field"> <div class="van-cell__title van-field__label"> @@ -360,24 +298,6 @@ exports[`should valid postal code and render error message correctly 1`] = ` </div> `; -exports[`should valid postal code and render error message correctly 2`] = ` -<div class="van-cell van-field"> - <div class="van-cell__title van-field__label"> - <label> - Postal - </label> - </div> - <div class="van-cell__value van-field__value"> - <div class="van-field__body"> - <input type="tel" - class="van-field__control" - placeholder="Postal" - > - </div> - </div> -</div> -`; - exports[`should valid tel and render error message correctly 1`] = ` <div class="van-cell van-field"> <div class="van-cell__title van-field__label"> @@ -398,21 +318,3 @@ exports[`should valid tel and render error message correctly 1`] = ` </div> </div> `; - -exports[`should valid tel and render error message correctly 2`] = ` -<div class="van-cell van-field"> - <div class="van-cell__title van-field__label"> - <label> - Phone - </label> - </div> - <div class="van-cell__value van-field__value"> - <div class="van-field__body"> - <input type="tel" - class="van-field__control" - placeholder="Phone" - > - </div> - </div> -</div> -`; diff --git a/packages/vant/src/address-edit/test/index.spec.js b/packages/vant/src/address-edit/test/index.spec.js index 21d42db66..492718977 100644 --- a/packages/vant/src/address-edit/test/index.spec.js +++ b/packages/vant/src/address-edit/test/index.spec.js @@ -1,6 +1,7 @@ import { AddressEdit } from '..'; import { areaList } from '../../area/demo/area-simple'; import { mount, later, trigger } from '../../../test'; +import { submitForm } from '../../form/test/shared'; const defaultAddressInfo = { name: '测试', @@ -27,12 +28,10 @@ const createComponent = (addressInfo = {}) => { }, }); - const button = wrapper.find('.van-button'); const fields = wrapper.findAll('.van-field'); return { vm: wrapper.vm, fields, - button, wrapper, }; }; @@ -55,13 +54,6 @@ test('should render AddressEdit with props correctly', () => { expect(wrapper.html()).toMatchSnapshot(); }); -// test('set-default', () => { -// const { wrapper } = createComponent(); -// wrapper.find('.van-switch').trigger('click'); - -// expect(wrapper.html()).toMatchSnapshot(); -// }); - test('should allow to custom validator with validator prop', async () => { const wrapper = mount(AddressEdit, { props: { @@ -70,63 +62,53 @@ test('should allow to custom validator with validator prop', async () => { }, }); - const button = wrapper.find('.van-button'); - await button.trigger('click'); + await submitForm(wrapper); expect(wrapper.find('.van-field__error-message').html()).toMatchSnapshot(); }); test('should valid name and render error message correctly', async () => { - const { fields, button } = createComponent({ + const { fields, wrapper } = createComponent({ name: '', }); - await button.trigger('click'); - expect(fields[0].html()).toMatchSnapshot(); - await fields[0].find('input').trigger('focus'); + await submitForm(wrapper); expect(fields[0].html()).toMatchSnapshot(); }); test('should valid tel and render error message correctly', async () => { - const { fields, button } = createComponent({ + const { fields, wrapper } = createComponent({ tel: '', }); - await button.trigger('click'); - expect(fields[1].html()).toMatchSnapshot(); - await fields[1].find('input').trigger('focus'); + await submitForm(wrapper); expect(fields[1].html()).toMatchSnapshot(); }); test('should valid area code and render error message correctly', async () => { - const { fields, button } = createComponent({ + const { fields, wrapper } = createComponent({ areaCode: '', }); - await button.trigger('click'); - expect(fields[2].html()).toMatchSnapshot(); - await fields[2].find('input').trigger('focus'); + await submitForm(wrapper); expect(fields[2].html()).toMatchSnapshot(); }); test('should valid address detail and render error message correctly', async () => { - const { fields, button } = createComponent({ + const { fields, wrapper } = createComponent({ addressDetail: '', }); - await button.trigger('click'); - expect(fields[3].html()).toMatchSnapshot(); - await fields[3].find('textarea').trigger('focus'); + await submitForm(wrapper); + await later(); expect(fields[3].html()).toMatchSnapshot(); }); test('should valid postal code and render error message correctly', async () => { - const { fields, button } = createComponent({ + const { fields, wrapper } = createComponent({ postalCode: '123', }); - await button.trigger('click'); - expect(fields[4].html()).toMatchSnapshot(); - await fields[4].find('input').trigger('focus'); + await submitForm(wrapper); expect(fields[4].html()).toMatchSnapshot(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e8a7c749..8767d7c8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,7 @@ importers: '@vant/use': ^1.3.3 '@vue/compiler-sfc': ^3.2.20 '@vue/runtime-core': ^3.2.20 + '@vue/test-utils': ^2.0.0-rc.16 typescript: 4.x vue: ^3.2.20 vue-router: ^4.0.12 @@ -68,6 +69,7 @@ importers: '@vant/cli': link:../vant-cli '@vue/compiler-sfc': 3.2.21 '@vue/runtime-core': 3.2.21 + '@vue/test-utils': 2.0.0-rc.16_vue@3.2.21 typescript: 4.4.4 vue: 3.2.21 vue-router: 4.0.12_vue@3.2.21 @@ -2477,6 +2479,14 @@ packages: vue: 3.2.21 dev: false + /@vue/test-utils/2.0.0-rc.16_vue@3.2.21: + resolution: {integrity: sha1-WTgPAocPhWrAAqKcAmgdPz/Lr+s=, tarball: '@vue/test-utils/download/@vue/test-utils-2.0.0-rc.16.tgz'} + peerDependencies: + vue: ^3.0.1 + dependencies: + vue: 3.2.21 + dev: true + /JSONStream/1.3.5: resolution: {integrity: sha1-MgjB8I06TZkmGrZPkjArwV4RHKA=, tarball: JSONStream/download/JSONStream-1.3.5.tgz} hasBin: true