From c952321fcf63c20bddd7f8b8bb08d5d8d5c21e96 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 5 Feb 2020 14:23:03 -0500 Subject: [PATCH] wip(compiler-ssr): v-model static types + textarea --- packages/compiler-core/src/ast.ts | 1 + .../compiler-core/src/transforms/vModel.ts | 13 +- packages/compiler-core/src/transforms/vOn.ts | 2 +- packages/compiler-dom/src/errors.ts | 2 + packages/compiler-dom/src/index.ts | 2 +- .../compiler-dom/src/transforms/vModel.ts | 124 ++++++++++-------- .../compiler-ssr/__tests__/ssrVModel.spec.ts | 68 ++++++++++ packages/compiler-ssr/src/runtimeHelpers.ts | 6 +- .../compiler-ssr/src/transforms/ssrVModel.ts | 122 ++++++++++++++++- .../compiler-ssr/src/transforms/ssrVShow.ts | 8 +- packages/runtime-dom/src/directives/vModel.ts | 43 +----- packages/server-renderer/src/index.ts | 6 + packages/shared/src/index.ts | 1 + packages/shared/src/looseEqual.ts | 42 ++++++ packages/vue/src/index.ts | 2 + 15 files changed, 328 insertions(+), 114 deletions(-) create mode 100644 packages/compiler-ssr/__tests__/ssrVModel.spec.ts create mode 100644 packages/shared/src/looseEqual.ts diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 97d64b203..cd605832e 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -204,6 +204,7 @@ export interface CompoundExpressionNode extends Node { type: NodeTypes.COMPOUND_EXPRESSION children: ( | SimpleExpressionNode + | CompoundExpressionNode | InterpolationNode | TextNode | string diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 8e13cc10a..7211334c3 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -56,11 +56,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { // "onUpdate:modelValue": $event => (foo = $event) createObjectProperty( eventName, - createCompoundExpression([ - `$event => (`, - ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children), - ` = $event)` - ]) + createCompoundExpression([`$event => (`, exp, ` = $event)`]) ) ] @@ -82,12 +78,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { const modifiersKey = arg ? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic ? `${arg.content}Modifiers` - : createCompoundExpression([ - ...(arg.type === NodeTypes.SIMPLE_EXPRESSION - ? [arg] - : arg.children), - ' + "Modifiers"' - ]) + : createCompoundExpression([arg, ' + "Modifiers"']) : `modelModifiers` props.push( createObjectProperty( diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index d6ecd1458..c56b300c0 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -87,7 +87,7 @@ export const transformOn: DirectiveTransform = ( // wrap inline statement in a function expression exp = createCompoundExpression([ `$event => ${hasMultipleStatements ? `{` : `(`}`, - ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children), + exp, hasMultipleStatements ? `}` : `)` ]) } diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts index 87579a8cd..8ead25260 100644 --- a/packages/compiler-dom/src/errors.ts +++ b/packages/compiler-dom/src/errors.ts @@ -28,6 +28,7 @@ export const enum DOMErrorCodes { X_V_MODEL_ON_INVALID_ELEMENT, X_V_MODEL_ARG_ON_ELEMENT, X_V_MODEL_ON_FILE_INPUT_ELEMENT, + X_V_MODEL_UNNECESSARY_VALUE, X_V_SHOW_NO_EXPRESSION, __EXTEND_POINT__ } @@ -40,5 +41,6 @@ export const DOMErrorMessages: { [code: number]: string } = { [DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on , `).code) + .toMatchInlineSnapshot(` + "const { _interpolate } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _push(\`\`) + }" + `) + }) +}) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 392b0eadc..87fd6b35f 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -9,6 +9,8 @@ export const SSR_RENDER_ATTRS = Symbol(`renderAttrs`) export const SSR_RENDER_ATTR = Symbol(`renderAttr`) export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`) export const SSR_RENDER_LIST = Symbol(`renderList`) +export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`) +export const SSR_LOOSE_CONTAIN = Symbol(`looseContain`) export const ssrHelpers = { [SSR_INTERPOLATE]: `_interpolate`, @@ -19,7 +21,9 @@ export const ssrHelpers = { [SSR_RENDER_ATTRS]: `_renderAttrs`, [SSR_RENDER_ATTR]: `_renderAttr`, [SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`, - [SSR_RENDER_LIST]: `_renderList` + [SSR_RENDER_LIST]: `_renderList`, + [SSR_LOOSE_EQUAL]: `_looseEqual`, + [SSR_LOOSE_CONTAIN]: `_looseContain` } // Note: these are helpers imported from @vue/server-renderer diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index 077991d83..8d4c0ad6d 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -1,7 +1,123 @@ -import { DirectiveTransform } from '@vue/compiler-dom' +import { + DirectiveTransform, + ElementTypes, + transformModel, + findProp, + NodeTypes, + createDOMCompilerError, + DOMErrorCodes, + Property, + createObjectProperty, + createSimpleExpression, + createCallExpression, + PlainElementNode, + ExpressionNode, + createConditionalExpression, + createInterpolation +} from '@vue/compiler-dom' +import { SSR_LOOSE_EQUAL, SSR_LOOSE_CONTAIN } from '../runtimeHelpers' export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { - return { - props: [] + const model = dir.exp! + + function checkDuplicatedValue() { + const value = findProp(node, 'value') + if (value) { + context.onError( + createDOMCompilerError( + DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE, + value.loc + ) + ) + } + } + + if (node.tagType === ElementTypes.ELEMENT) { + let props: Property[] = [] + const defaultProps = [ + // default value binding for text type inputs + createObjectProperty(createSimpleExpression(`value`, true), model) + ] + if (node.tag === 'input') { + const type = findProp(node, 'type') + if (type) { + if (type.type === NodeTypes.DIRECTIVE) { + // dynamic type + // TODO + } else if (type.value) { + // static type + switch (type.value.content) { + case 'radio': + props = [ + createObjectProperty( + createSimpleExpression(`checked`, true), + createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ + model, + findValueBinding(node) + ]) + ) + ] + break + case 'checkbox': + const value = findValueBinding(node) + props = [ + createObjectProperty( + createSimpleExpression(`checked`, true), + createConditionalExpression( + createCallExpression(`Array.isArray`, [model]), + createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ + model, + value + ]), + model + ) + ) + ] + break + case 'file': + context.onError( + createDOMCompilerError( + DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT, + dir.loc + ) + ) + break + default: + checkDuplicatedValue() + props = defaultProps + break + } + } + } else { + checkDuplicatedValue() + props = defaultProps + } + } else if (node.tag === 'textarea') { + checkDuplicatedValue() + node.children = [createInterpolation(model, model.loc)] + } else if (node.tag === 'select') { + // TODO + } else { + context.onError( + createDOMCompilerError( + DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT, + dir.loc + ) + ) + } + + return { props } + } else { + // component v-model + return transformModel(dir, node, context) } } + +function findValueBinding(node: PlainElementNode): ExpressionNode { + const valueBinding = findProp(node, 'value') + return valueBinding + ? valueBinding.type === NodeTypes.DIRECTIVE + ? valueBinding.exp! + : createSimpleExpression(valueBinding.value!.content, true) + : createSimpleExpression(`null`, false) +} diff --git a/packages/compiler-ssr/src/transforms/ssrVShow.ts b/packages/compiler-ssr/src/transforms/ssrVShow.ts index d46cac737..314d8a234 100644 --- a/packages/compiler-ssr/src/transforms/ssrVShow.ts +++ b/packages/compiler-ssr/src/transforms/ssrVShow.ts @@ -1,16 +1,18 @@ import { DirectiveTransform, - createCompilerError, DOMErrorCodes, createObjectProperty, createSimpleExpression, createConditionalExpression, - createObjectExpression + createObjectExpression, + createDOMCompilerError } from '@vue/compiler-dom' export const ssrTransformShow: DirectiveTransform = (dir, node, context) => { if (!dir.exp) { - context.onError(createCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION)) + context.onError( + createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION) + ) } return { props: [ diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 8cc9ea7b8..8b1343944 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -5,7 +5,7 @@ import { warn } from '@vue/runtime-core' import { addEventListener } from '../modules/events' -import { isArray, isObject } from '@vue/shared' +import { isArray, looseEqual, looseIndexOf } from '@vue/shared' const getModelAssigner = (vnode: VNode): ((value: any) => void) => vnode.props!['onUpdate:modelValue'] @@ -182,47 +182,6 @@ function setSelected(el: HTMLSelectElement, value: any) { } } -function looseEqual(a: any, b: any): boolean { - if (a === b) return true - const isObjectA = isObject(a) - const isObjectB = isObject(b) - if (isObjectA && isObjectB) { - try { - const isArrayA = isArray(a) - const isArrayB = isArray(b) - if (isArrayA && isArrayB) { - return ( - a.length === b.length && - a.every((e: any, i: any) => looseEqual(e, b[i])) - ) - } else if (a instanceof Date && b instanceof Date) { - return a.getTime() === b.getTime() - } else if (!isArrayA && !isArrayB) { - const keysA = Object.keys(a) - const keysB = Object.keys(b) - return ( - keysA.length === keysB.length && - keysA.every(key => looseEqual(a[key], b[key])) - ) - } else { - /* istanbul ignore next */ - return false - } - } catch (e) { - /* istanbul ignore next */ - return false - } - } else if (!isObjectA && !isObjectB) { - return String(a) === String(b) - } else { - return false - } -} - -function looseIndexOf(arr: any[], val: any): number { - return arr.findIndex(item => looseEqual(item, val)) -} - // retrieve raw value set via :value bindings function getValue(el: HTMLOptionElement | HTMLInputElement) { return '_value' in el ? (el as any)._value : el.value diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 39e5c3e33..099a2e034 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -15,3 +15,9 @@ export { } from './helpers/renderAttrs' export { interpolate as _interpolate } from './helpers/interpolate' export { renderList as _renderList } from './helpers/renderList' + +// v-model helpers +import { looseEqual, looseIndexOf } from '@vue/shared' +export const _looseEqual = looseEqual as (a: unknown, b: unknown) => boolean +export const _looseContain = (arr: unknown[], value: unknown): boolean => + looseIndexOf(arr, value) > -1 diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 3a1c07f2d..4ec5d8d91 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -9,6 +9,7 @@ export * from './normalizeProp' export * from './domTagConfig' export * from './domAttrConfig' export * from './escapeHtml' +export * from './looseEqual' export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__ ? Object.freeze({}) diff --git a/packages/shared/src/looseEqual.ts b/packages/shared/src/looseEqual.ts new file mode 100644 index 000000000..bd58332dd --- /dev/null +++ b/packages/shared/src/looseEqual.ts @@ -0,0 +1,42 @@ +import { isObject, isArray } from './' + +export function looseEqual(a: any, b: any): boolean { + if (a === b) return true + const isObjectA = isObject(a) + const isObjectB = isObject(b) + if (isObjectA && isObjectB) { + try { + const isArrayA = isArray(a) + const isArrayB = isArray(b) + if (isArrayA && isArrayB) { + return ( + a.length === b.length && + a.every((e: any, i: any) => looseEqual(e, b[i])) + ) + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + const keysA = Object.keys(a) + const keysB = Object.keys(b) + return ( + keysA.length === keysB.length && + keysA.every(key => looseEqual(a[key], b[key])) + ) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } +} + +export function looseIndexOf(arr: any[], val: any): number { + return arr.findIndex(item => looseEqual(item, val)) +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index f79b76960..2cc408889 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -47,6 +47,8 @@ function compileToFunction( err.loc.end.offset ) warn(codeFrame ? `${message}\n${codeFrame}` : message) + } else { + throw err } }, ...options