From: 宋铄运 Date: Mon, 14 Oct 2019 04:33:23 +0000 (+0800) Subject: feat(dom): transform + runtime for v-on (#213) X-Git-Tag: v3.0.0-alpha.0~455 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=57a94b530d40904fafdc9783c3e957c64c9db465;p=thirdparty%2Fvuejs%2Fcore.git feat(dom): transform + runtime for v-on (#213) --- diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 3e8c74c25b..dbdc627531 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -1,5 +1,6 @@ import { DirectiveTransform } from '../transform' import { + DirectiveNode, createObjectProperty, createSimpleExpression, ExpressionNode, @@ -14,12 +15,22 @@ import { isMemberExpression } from '../utils' const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/ -// v-on without arg is handled directly in ./element.ts due to it affecting -// codegen for the entire props object. This transform here is only for v-on -// *with* args. -export const transformOn: DirectiveTransform = (dir, node, context) => { - const { loc, modifiers } = dir - const arg = dir.arg! +export interface VOnDirectiveNode extends DirectiveNode { + // v-on without arg is handled directly in ./element.ts due to it affecting + // codegen for the entire props object. This transform here is only for v-on + // *with* args. + arg: ExpressionNode + // exp is guaranteed to be a simple expression here because v-on w/ arg is + // skipped by transformExpression as a special case. + exp: SimpleExpressionNode | undefined +} + +export const transformOn: DirectiveTransform = ( + dir: VOnDirectiveNode, + node, + context +) => { + const { loc, modifiers, arg } = dir if (!dir.exp && !modifiers.length) { context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc)) } @@ -44,10 +55,8 @@ export const transformOn: DirectiveTransform = (dir, node, context) => { // other modifiers are handled in compiler-dom // handler processing - if (dir.exp) { - // exp is guaranteed to be a simple expression here because v-on w/ arg is - // skipped by transformExpression as a special case. - let exp: ExpressionNode = dir.exp as SimpleExpressionNode + let exp: ExpressionNode | undefined = dir.exp + if (exp) { const isInlineStatement = !( isMemberExpression(exp.content) || fnExpRE.test(exp.content) ) @@ -65,14 +74,13 @@ export const transformOn: DirectiveTransform = (dir, node, context) => { `)` ]) } - dir.exp = exp } return { props: [ createObjectProperty( eventName, - dir.exp || createSimpleExpression(`() => {}`, false, loc) + exp || createSimpleExpression(`() => {}`, false, loc) ) ], needRuntime: false diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts new file mode 100644 index 0000000000..c730b98651 --- /dev/null +++ b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts @@ -0,0 +1,96 @@ +import { + parse, + transform, + CompilerOptions, + ElementNode, + ObjectExpression, + CallExpression, + NodeTypes, + Property +} from '@vue/compiler-core' +import { transformOn } from '../../src/transforms/vOn' +import { V_ON_MODIFIERS_GUARD, V_ON_KEYS_GUARD } from '../../src/runtimeHelpers' +import { transformElement } from '../../../compiler-core/src/transforms/transformElement' +import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression' +import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils' + +function parseVOnProperties( + template: string, + options: CompilerOptions = {} +): Property[] { + const ast = parse(template) + transform(ast, { + nodeTransforms: [transformExpression, transformElement], + directiveTransforms: { + on: transformOn + }, + ...options + }) + return (((ast.children[0] as ElementNode).codegenNode as CallExpression) + .arguments[1] as ObjectExpression).properties +} + +describe('compiler-dom: transform v-on', () => { + it('should support muliple modifiers w/ prefixIdentifiers: true', () => { + const [prop] = parseVOnProperties(`
`, { + prefixIdentifiers: true + }) + expect(prop).toMatchObject({ + type: NodeTypes.JS_PROPERTY, + value: createObjectMatcher({ + handler: { + callee: V_ON_MODIFIERS_GUARD, + arguments: [{ content: '_ctx.test' }, '["stop","prevent"]'] + }, + persistent: { content: 'true', isStatic: false } + }) + }) + }) + + it('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => { + const [prop] = parseVOnProperties( + `
`, + { prefixIdentifiers: true } + ) + expect(prop).toMatchObject({ + type: NodeTypes.JS_PROPERTY, + value: createObjectMatcher({ + handler: { + callee: V_ON_MODIFIERS_GUARD, + arguments: [{ content: '_ctx.test' }, '["stop"]'] + }, + persistent: { content: 'true', isStatic: false }, + options: createObjectMatcher({ + capture: { content: 'true', isStatic: false }, + passive: { content: 'true', isStatic: false } + }) + }) + }) + }) + + it('should wrap keys guard for keyboard events or dynamic events', () => { + const [prop] = parseVOnProperties( + `
`, + { prefixIdentifiers: true } + ) + expect(prop).toMatchObject({ + type: NodeTypes.JS_PROPERTY, + value: createObjectMatcher({ + handler: { + callee: V_ON_KEYS_GUARD, + arguments: [ + { + callee: V_ON_MODIFIERS_GUARD, + arguments: [{ content: '_ctx.test' }, '["stop","ctrl"]'] + }, + '["a"]' + ] + }, + persistent: { content: 'true', isStatic: false }, + options: createObjectMatcher({ + capture: { content: 'true', isStatic: false } + }) + }) + }) + }) +}) diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 63784edb4b..3b3d002dc5 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -6,6 +6,7 @@ import { transformCloak } from './transforms/vCloak' import { transformVHtml } from './transforms/vHtml' import { transformVText } from './transforms/vText' import { transformModel } from './transforms/vModel' +import { transformOn } from './transforms/vOn' export function compile( template: string, @@ -20,6 +21,7 @@ export function compile( html: transformVHtml, text: transformVText, model: transformModel, // override compiler-core + on: transformOn, ...(options.directiveTransforms || {}) } }) diff --git a/packages/compiler-dom/src/runtimeHelpers.ts b/packages/compiler-dom/src/runtimeHelpers.ts index 5d93c1e08f..c1405498cd 100644 --- a/packages/compiler-dom/src/runtimeHelpers.ts +++ b/packages/compiler-dom/src/runtimeHelpers.ts @@ -6,10 +6,15 @@ export const V_MODEL_TEXT = Symbol(__DEV__ ? `vModelText` : ``) export const V_MODEL_SELECT = Symbol(__DEV__ ? `vModelSelect` : ``) export const V_MODEL_DYNAMIC = Symbol(__DEV__ ? `vModelDynamic` : ``) +export const V_ON_MODIFIERS_GUARD = Symbol(__DEV__ ? `vOnModifiersGuard` : ``) +export const V_ON_KEYS_GUARD = Symbol(__DEV__ ? `vOnKeysGuard` : ``) + registerRuntimeHelpers({ [V_MODEL_RADIO]: `vModelRadio`, [V_MODEL_CHECKBOX]: `vModelCheckbox`, [V_MODEL_TEXT]: `vModelText`, [V_MODEL_SELECT]: `vModelSelect`, - [V_MODEL_DYNAMIC]: `vModelDynamic` + [V_MODEL_DYNAMIC]: `vModelDynamic`, + [V_ON_MODIFIERS_GUARD]: `vOnModifiersGuard`, + [V_ON_KEYS_GUARD]: `vOnKeysGuard` }) diff --git a/packages/compiler-dom/src/transforms/vOn.ts b/packages/compiler-dom/src/transforms/vOn.ts index 70b786d12e..73e7fdb945 100644 --- a/packages/compiler-dom/src/transforms/vOn.ts +++ b/packages/compiler-dom/src/transforms/vOn.ts @@ -1 +1,81 @@ -// TODO +import { + transformOn as baseTransform, + DirectiveTransform, + createObjectProperty, + createCallExpression, + createObjectExpression, + createSimpleExpression, + NodeTypes +} from '@vue/compiler-core' +import { V_ON_MODIFIERS_GUARD, V_ON_KEYS_GUARD } from '../runtimeHelpers' + +const EVENT_OPTION_MODIFIERS = { passive: true, once: true, capture: true } +const NOT_KEY_MODIFIERS = { + stop: true, + prevent: true, + self: true, + // system + ctrl: true, + shift: true, + alt: true, + meta: true, + // mouse + left: true, + middle: true, + right: true, + // exact + exact: true +} +const KEYBOARD_EVENTS = { onkeyup: true, onkeydown: true, onkeypress: true } + +export const transformOn: DirectiveTransform = (dir, node, context) => { + const { modifiers } = dir + const baseResult = baseTransform(dir, node, context) + if (!modifiers.length) return baseResult + const { key, value } = baseResult.props[0] + const runtimeModifiers = modifiers.filter(m => !(m in EVENT_OPTION_MODIFIERS)) + let handler = createCallExpression(context.helper(V_ON_MODIFIERS_GUARD), [ + value, + JSON.stringify(runtimeModifiers.filter(m => m in NOT_KEY_MODIFIERS)) + ]) + if ( + // if event name is dynamic, always wrap with keys guard + key.type === NodeTypes.COMPOUND_EXPRESSION || + !(key.isStatic) || + key.content.toLowerCase() in KEYBOARD_EVENTS + ) { + handler = createCallExpression(context.helper(V_ON_KEYS_GUARD), [ + handler, + JSON.stringify(runtimeModifiers.filter(m => !(m in NOT_KEY_MODIFIERS))) + ]) + } + const properties = [ + createObjectProperty('handler', handler), + // so the runtime knows the options never change + createObjectProperty('persistent', createSimpleExpression('true', false)) + ] + + const eventOptionModifiers = modifiers.filter( + modifier => modifier in EVENT_OPTION_MODIFIERS + ) + if (eventOptionModifiers.length) { + properties.push( + createObjectProperty( + 'options', + createObjectExpression( + eventOptionModifiers.map(modifier => + createObjectProperty( + modifier, + createSimpleExpression('true', false) + ) + ) + ) + ) + ) + } + + return { + props: [createObjectProperty(key, createObjectExpression(properties))], + needRuntime: false + } +} diff --git a/packages/runtime-dom/__tests__/directives/vOn.spec.ts b/packages/runtime-dom/__tests__/directives/vOn.spec.ts new file mode 100644 index 0000000000..336839dda5 --- /dev/null +++ b/packages/runtime-dom/__tests__/directives/vOn.spec.ts @@ -0,0 +1,57 @@ +import { patchEvent } from '../../src/modules/events' +import { vOnModifiersGuard } from '@vue/runtime-dom' + +function triggerEvent( + target: Element, + event: string, + process?: (e: any) => any +) { + const e = document.createEvent('HTMLEvents') + e.initEvent(event, true, true) + if (event === 'click') { + ;(e as any).button = 0 + } + if (process) process(e) + target.dispatchEvent(e) + return e +} + +describe('runtime-dom: v-on directive', () => { + test('it should support stop and prevent', async () => { + const parent = document.createElement('div') + const child = document.createElement('input') + parent.appendChild(child) + const childNextValue = { + handler: vOnModifiersGuard(jest.fn(), ['prevent', 'stop']), + options: {} + } + patchEvent(child, 'click', null, childNextValue, null) + const parentHandler = jest.fn() + const parentNextValue = { handler: parentHandler, options: {} } + patchEvent(parent, 'click', null, parentNextValue, null) + expect(triggerEvent(child, 'click').defaultPrevented).toBe(true) + expect(parentHandler).not.toBeCalled() + }) + + test('it should support key modifiers and system modifiers', () => { + const el = document.createElement('div') + const fn = jest.fn() + const nextValue = { + handler: vOnModifiersGuard(fn, ['ctrl', 'esc']), + options: {} + } + patchEvent(el, 'click', null, nextValue, null) + triggerEvent(el, 'click', e => { + e.ctrlKey = false + e.key = 'esc' + }) + expect(fn).not.toBeCalled() + triggerEvent(el, 'click', e => { + e.ctrlKey = true + e.key = 'Escape' + }) + expect(fn).toBeCalled() + }) + + test('it should support "exact" modifier', () => {}) +}) diff --git a/packages/runtime-dom/src/directives/vOn.ts b/packages/runtime-dom/src/directives/vOn.ts new file mode 100644 index 0000000000..b064799561 --- /dev/null +++ b/packages/runtime-dom/src/directives/vOn.ts @@ -0,0 +1,56 @@ +const systemModifiers = new Set(['ctrl', 'shift', 'alt', 'meta']) + +const modifierGuards: Record< + string, + (e: Event, modifiers?: string[]) => void | boolean +> = { + stop: e => e.stopPropagation(), + prevent: e => e.preventDefault(), + self: e => e.target !== e.currentTarget, + ctrl: e => !(e as any).ctrlKey, + shift: e => !(e as any).shiftKey, + alt: e => !(e as any).altKey, + meta: e => !(e as any).metaKey, + left: e => 'button' in e && (e as any).button !== 0, + middle: e => 'button' in e && (e as any).button !== 1, + right: e => 'button' in e && (e as any).button !== 2, + exact: (e, modifiers) => + modifiers!.some(m => systemModifiers.has(m) && (e as any)[`${m}Key`]) +} + +export const vOnModifiersGuard = (fn: Function, modifiers: string[]) => { + return (event: Event) => { + for (let i = 0; i < modifiers.length; i++) { + const guard = modifierGuards[modifiers[i]] + if (guard && guard(event, modifiers)) return + } + return fn(event) + } +} + + +// Kept for 2.x compat. +// Note: IE11 compat for `spacebar` and `del` is removed for now. +const keyNames: Record = { + esc: 'escape', + space: ' ', + up: 'arrowup', + left: 'arrowleft', + right: 'arrowright', + down: 'arrowdown', + delete: 'backspace' +} + +export const vOnKeysGuard = (fn: Function, modifiers: string[]) => { + return (event: KeyboardEvent) => { + if (!('key' in event)) return + const eventKey = event.key.toLowerCase() + if ( + // None of the provided key modifiers match the current event key + !modifiers.some(k => k === eventKey || keyNames[k] === eventKey) + ) { + return + } + return fn(event) + } +} diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index f8d0efda6a..ac293a0a3f 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -18,6 +18,8 @@ export { vModelDynamic } from './directives/vModel' +export { vOnModifiersGuard, vOnKeysGuard } from './directives/vOn' + // re-export everything from core // h, Component, reactivity API, nextTick, flags & types export * from '@vue/runtime-core'