From 52c12fce8ea6d9da3e5426f8b24480198d508b4e Mon Sep 17 00:00:00 2001 From: fxy060608 Date: Tue, 29 Mar 2022 16:09:27 +0800 Subject: [PATCH] fix(mp): v-model with modifiers (#3381) --- .../uni-mp-compiler/__tests__/vModel.spec.ts | 60 ++++++++++++++ .../uni-mp-compiler/src/runtimeHelpers.ts | 4 +- .../uni-mp-compiler/src/transforms/vModel.ts | 78 +++++++++++++++++-- packages/uni-mp-vue/dist/vue.runtime.esm.js | 29 ++++++- packages/uni-mp-vue/src/helpers/index.ts | 6 ++ packages/uni-mp-vue/src/helpers/vOn.ts | 2 +- .../src/helpers/withModelModifiers.ts | 36 +++++++++ 7 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 packages/uni-mp-vue/src/helpers/withModelModifiers.ts diff --git a/packages/uni-mp-compiler/__tests__/vModel.spec.ts b/packages/uni-mp-compiler/__tests__/vModel.spec.ts index ea3870ca2..b55e50ad9 100644 --- a/packages/uni-mp-compiler/__tests__/vModel.spec.ts +++ b/packages/uni-mp-compiler/__tests__/vModel.spec.ts @@ -22,6 +22,39 @@ describe('compiler: transform v-model', () => { } ) }) + test(`component v-model with number`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _o(_m($event => _ctx.model = $event, { number: true }, true)), b: _p({ modelValue: _ctx.model }) } +}`, + { + cacheHandlers: true, + } + ) + }) + test(`component v-model with trim`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _o(_m($event => _ctx.model = $event, { trim: true }, true)), b: _p({ modelValue: _ctx.model }) } +}`, + { + cacheHandlers: true, + } + ) + }) + test(`component v-model with number and trim`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _o(_m($event => _ctx.model = $event, { trim: true, number: true }, true)), b: _p({ modelValue: _ctx.model }) } +}` + ) + }) test(`input,textarea v-model`, () => { assert( ``, @@ -44,6 +77,33 @@ describe('compiler: transform v-model', () => { ``, `(_ctx, _cache) => { return { a: _o([$event => _ctx.model = $event.detail.value, _ctx.input]), b: _ctx.model } +}` + ) + }) + test(`input v-model with number`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _ctx.model, b: _o(_m($event => _ctx.model = $event.detail.value, { number: true })) } +}` + ) + }) + test(`input v-model with number and trim`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _ctx.model, b: _o(_m($event => _ctx.model = $event.detail.value, { trim: true, number: true })) } +}` + ) + }) + test(`input v-model.number + v-on`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _o([_m($event => _ctx.model = $event.detail.value, { number: true }), _ctx.input]), b: _ctx.model } }` ) }) diff --git a/packages/uni-mp-compiler/src/runtimeHelpers.ts b/packages/uni-mp-compiler/src/runtimeHelpers.ts index 7600dbfac..cf6dd7d3b 100644 --- a/packages/uni-mp-compiler/src/runtimeHelpers.ts +++ b/packages/uni-mp-compiler/src/runtimeHelpers.ts @@ -13,6 +13,7 @@ export const WITH_SCOPED_SLOT = Symbol(`withScopedSlot`) export const STRINGIFY_STYLE = Symbol(`stringifyStyle`) export const NORMALIZE_CLASS = Symbol(`normalizeClass`) export const TO_DISPLAY_STRING = Symbol(`toDisplayString`) +export const WITH_MODEL_MODIFIERS = Symbol(`withModelModifiers`) registerRuntimeHelpers({ [V_ON]: 'o', [V_FOR]: 'f', @@ -26,5 +27,6 @@ registerRuntimeHelpers({ [WITH_SCOPED_SLOT]: 'w', [STRINGIFY_STYLE]: 's', [NORMALIZE_CLASS]: 'n', - [TO_DISPLAY_STRING]: `t`, + [TO_DISPLAY_STRING]: 't', + [WITH_MODEL_MODIFIERS]: 'm', }) diff --git a/packages/uni-mp-compiler/src/transforms/vModel.ts b/packages/uni-mp-compiler/src/transforms/vModel.ts index c33812086..5d67b7f86 100644 --- a/packages/uni-mp-compiler/src/transforms/vModel.ts +++ b/packages/uni-mp-compiler/src/transforms/vModel.ts @@ -12,6 +12,11 @@ import { createCompoundExpression, DirectiveTransform, TransformContext as VueTransformContext, + isSimpleIdentifier, + isStaticExp, + createObjectProperty, + createSimpleExpression, + ConstantTypes, } from '@vue/compiler-core' import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom' @@ -19,8 +24,9 @@ import { createBindDirectiveNode, createOnDirectiveNode, ATTR_DATASET_EVENT_OPTS, + isSimpleExpressionNode, } from '@dcloudio/uni-cli-shared' -import { V_ON } from '../runtimeHelpers' +import { V_ON, WITH_MODEL_MODIFIERS } from '../runtimeHelpers' import { genExpr } from '../codegen' import { TransformContext } from '../transform' import { DirectiveTransformResult } from './transformElement' @@ -75,6 +81,29 @@ export const transformModel: DirectiveTransform = ( ) } + if (dir.modifiers.length) { + const arg = dir.arg + const modifiers = dir.modifiers + .map((m) => (isSimpleIdentifier(m) ? m : JSON.stringify(m)) + `: true`) + .join(`, `) + const modifiersKey = arg + ? isStaticExp(arg) + ? `${arg.content}Modifiers` + : createCompoundExpression([arg, ' + "Modifiers"']) + : `modelModifiers` + baseResult.props.push( + createObjectProperty( + modifiersKey, + createSimpleExpression( + `{ ${modifiers} }`, + false, + dir.loc, + ConstantTypes.CAN_HOIST + ) + ) + ) + } + return transformElementVModel( baseResult.props, node, @@ -98,6 +127,7 @@ function transformElementVModel( context: TransformContext ) { const dirs = transformVModel(props, node, context, { + isComponent: false, binding: 'value', event: 'input', formatEventCode(code) { @@ -230,6 +260,7 @@ function transformComponentVModel( ) { return { props: transformVModel(props, node, context, { + isComponent: true, formatEventCode(code) { return code }, @@ -242,16 +273,18 @@ function transformVModel( node: ElementNode, context: TransformContext, { + isComponent, binding, event, formatEventCode, }: { + isComponent: boolean binding?: string event?: string formatEventCode: (code: string) => string } ) { - if (props.length !== 2) { + if (props.length < 2) { return [] } const { key: modelValueArg, value: modelValeExpr } = props[0] @@ -270,15 +303,22 @@ function transformVModel( binding || modelValueArg.content, genExpr(modelValeExpr as ExpressionNode) ) + const modifiers = parseVModelModifiers(props[2]) + // onUpdateExpr 通常是 ExpressionNode 或者被 cache 的 ExpressionNode + const vOnValue = ( + onUpdateExpr.type === NodeTypes.JS_CACHE_EXPRESSION + ? onUpdateExpr.value + : onUpdateExpr + ) as ExpressionNode + const vOnUpdate = createOnDirectiveNode( event || camelize(onUpdateArg.content.replace('onUpdate:', 'update-')), formatEventCode( genExpr( wrapperVOn( - // onUpdateExpr 通常是 ExpressionNode 或者被 cache 的 ExpressionNode - (onUpdateExpr.type === NodeTypes.JS_CACHE_EXPRESSION - ? onUpdateExpr.value - : onUpdateExpr) as ExpressionNode, + modifiers + ? wrapperVModelModifiers(vOnValue, modifiers, context, isComponent) + : vOnValue, node, context ) @@ -287,3 +327,29 @@ function transformVModel( ) return [vBindModelValue, vOnUpdate] } + +function parseVModelModifiers(property?: Property) { + if ( + property && + isSimpleExpressionNode(property.key) && + property.key.content.endsWith('Modifiers') && + isSimpleExpressionNode(property.value) + ) { + return property.value.content + } +} +function wrapperVModelModifiers( + exp: ExpressionNode, + modifiers: string, + context: TransformContext, + isComponent = false +) { + return createCompoundExpression([ + `${context.helperString(WITH_MODEL_MODIFIERS)}(`, + exp, + ',', + modifiers, + `${isComponent ? `, true` : ``}`, + `)`, + ]) +} diff --git a/packages/uni-mp-vue/dist/vue.runtime.esm.js b/packages/uni-mp-vue/dist/vue.runtime.esm.js index 99b91e76f..8d94c8e73 100644 --- a/packages/uni-mp-vue/dist/vue.runtime.esm.js +++ b/packages/uni-mp-vue/dist/vue.runtime.esm.js @@ -5563,6 +5563,30 @@ function setRef(ref, id, opts = {}) { $templateRefs.push({ i: id, r: ref, k: opts.k, f: opts.f }); } +function withModelModifiers(fn, { number, trim }, isComponent = false) { + if (isComponent) { + return (...args) => { + if (trim) { + args = args.map((a) => a.trim()); + } + else if (number) { + args = args.map(toNumber); + } + return fn(...args); + }; + } + return (event) => { + const value = event.detail.value; + if (trim) { + event.detail.value = value.trim(); + } + else if (number) { + event.detail.value = toNumber(value); + } + return fn(event); + }; +} + function setupDevtoolsPlugin() { // noop } @@ -5579,7 +5603,8 @@ const h = (str) => hyphenate(str); const n = (value) => normalizeClass(value); const t = (val) => toDisplayString(val); const p = (props) => renderProps(props); -const sr = (ref, id, opts) => setRef(ref, id, opts); +const sr = (ref, id, opts) => setRef(ref, id, opts); +const m = (fn, modifiers, isComponent = false) => withModelModifiers(fn, modifiers, isComponent); function createApp(rootComponent, rootProps = null) { rootComponent && (rootComponent.mpType = 'app'); @@ -5587,4 +5612,4 @@ function createApp(rootComponent, rootProps = null) { } const createSSRApp = createApp; -export { EffectScope, Fragment, ReactiveEffect, Text, c, callWithAsyncErrorHandling, callWithErrorHandling, computed$1 as computed, createApp, createSSRApp, createVNode$1 as createVNode, createVueApp, customRef, d, defineAsyncComponent, defineComponent, defineEmits, defineExpose, defineProps, diff, e, effect, effectScope, f, findComponentPropsData, getCurrentInstance, getCurrentScope, getExposeProxy, guardReactiveProps, h, inject, injectHook, invalidateJob, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, n, nextTick, o, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onServerPrefetch, onUnmounted, onUpdated, p, patch, provide, proxyRefs, pruneComponentPropsCache, queuePostFlushCb, r, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, s, setCurrentRenderingInstance, setTemplateRef, setupDevtoolsPlugin, shallowReactive, shallowReadonly, shallowRef, sr, stop, t, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, updateProps, useAttrs, useCssModule, useCssVars, useSSRContext, useSlots, version, w, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId }; +export { EffectScope, Fragment, ReactiveEffect, Text, c, callWithAsyncErrorHandling, callWithErrorHandling, computed$1 as computed, createApp, createSSRApp, createVNode$1 as createVNode, createVueApp, customRef, d, defineAsyncComponent, defineComponent, defineEmits, defineExpose, defineProps, diff, e, effect, effectScope, f, findComponentPropsData, getCurrentInstance, getCurrentScope, getExposeProxy, guardReactiveProps, h, inject, injectHook, invalidateJob, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, m, markRaw, mergeDefaults, mergeProps, n, nextTick, o, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onServerPrefetch, onUnmounted, onUpdated, p, patch, provide, proxyRefs, pruneComponentPropsCache, queuePostFlushCb, r, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, s, setCurrentRenderingInstance, setTemplateRef, setupDevtoolsPlugin, shallowReactive, shallowReadonly, shallowRef, sr, stop, t, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, updateProps, useAttrs, useCssModule, useCssVars, useSSRContext, useSlots, version, w, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId }; diff --git a/packages/uni-mp-vue/src/helpers/index.ts b/packages/uni-mp-vue/src/helpers/index.ts index 542af4fbe..0f489aac2 100644 --- a/packages/uni-mp-vue/src/helpers/index.ts +++ b/packages/uni-mp-vue/src/helpers/index.ts @@ -13,6 +13,7 @@ import { stringifyStyle } from './style' import { dynamicSlot } from './dynamicSlot' import { setRef } from './ref' import { renderProps } from './renderProps' +import { withModelModifiers } from './withModelModifiers' export { setupDevtoolsPlugin } from './devtools' @@ -38,3 +39,8 @@ export const n: typeof normalizeClass = (value) => normalizeClass(value) export const t: typeof toDisplayString = (val) => toDisplayString(val) export const p: typeof renderProps = (props) => renderProps(props) export const sr: typeof setRef = (ref, id, opts) => setRef(ref, id, opts) +export const m: typeof withModelModifiers = ( + fn, + modifiers, + isComponent = false +) => withModelModifiers(fn, modifiers, isComponent) diff --git a/packages/uni-mp-vue/src/helpers/vOn.ts b/packages/uni-mp-vue/src/helpers/vOn.ts index 0039c928f..589d0a86e 100644 --- a/packages/uni-mp-vue/src/helpers/vOn.ts +++ b/packages/uni-mp-vue/src/helpers/vOn.ts @@ -56,7 +56,7 @@ export function vOn(value: EventValue | undefined, key?: number | string) { return name } -interface MPEvent extends WechatMiniprogram.BaseEvent { +export interface MPEvent extends WechatMiniprogram.BaseEvent { detail: Record & { __args__?: unknown[] } diff --git a/packages/uni-mp-vue/src/helpers/withModelModifiers.ts b/packages/uni-mp-vue/src/helpers/withModelModifiers.ts new file mode 100644 index 000000000..4dd3af1df --- /dev/null +++ b/packages/uni-mp-vue/src/helpers/withModelModifiers.ts @@ -0,0 +1,36 @@ +import { toNumber } from '@vue/shared' +import type { MPEvent } from './vOn' + +interface VModelFn { + (...args: unknown[]): unknown + modifiers?: VModelModifiers +} +interface VModelModifiers { + number?: boolean + trim?: boolean +} +export function withModelModifiers( + fn: VModelFn, + { number, trim }: VModelModifiers, + isComponent = false +) { + if (isComponent) { + return (...args: any[]) => { + if (trim) { + args = args.map((a) => a.trim()) + } else if (number) { + args = args.map(toNumber) + } + return fn(...args) + } + } + return (event: MPEvent) => { + const value: string = event.detail.value + if (trim) { + event.detail.value = value.trim() + } else if (number) { + event.detail.value = toNumber(value) + } + return fn(event) + } +} -- GitLab