Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add hook to transform h's arguments #851

Merged
merged 2 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions packages/runtime-core/__tests__/h.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { h } from '../src/h'
import { h, transformHArgs, resetTransformHArgs } from '../src/h'
import { createVNode } from '../src/vnode'
import { ComponentInternalInstance } from '@vue/runtime-core'
import { createApp } from '@vue/runtime-dom'

// Since h is a thin layer on top of createVNode, we are only testing its
// own logic here. Details of vnode creation is tested in vnode.spec.ts.
describe('renderer: h', () => {
const testH = () => {
test('type only', () => {
expect(h('div')).toMatchObject(createVNode('div'))
})
Expand Down Expand Up @@ -57,4 +59,62 @@ describe('renderer: h', () => {
})
)
})
}

describe('renderer: h', testH)

describe('renderer: transformHArgs', () => {
describe('no-op pass-through', () => {
beforeAll(() => {
transformHArgs((hArgs: unknown[]) => hArgs)
})

afterAll(resetTransformHArgs)

testH()
})

describe('args is used directly, without merging', () => {
beforeAll(() => {
transformHArgs(() => ['h1', 'Hello World'])
})

afterAll(resetTransformHArgs)

test('nodes become an h1 with text inside', () => {
expect(h('div')).toMatchObject(createVNode('h1', null, 'Hello World'))
})

test('resetting transformHArgs turns things back to normal', () => {
expect(h('div')).toMatchObject(createVNode('h1', null, 'Hello World'))
resetTransformHArgs()
expect(h('div')).toMatchObject(createVNode('div'))
})
})

test('receives component instance as the 2nd arg', () => {
transformHArgs((_: unknown[], instance: ComponentInternalInstance) => {
return ['h1', instance.type.name] // <h1>{{ name }}</h1>
})

const vm = createApp({
// this will be the name of the component in the h1
name: 'Root Component',
render() {
return h({
// this code will never execute,
// because it is overridden by the transformHArgs method
render() {
return h('h2', 'Stub Text')
}
})
}
})

// we need to mount everything so that the instance passed to
// transformHArgs isn't null
vm.mount('body')

expect(document.body.outerHTML).toContain('<h1>Root Component</h1>')
})
})
52 changes: 37 additions & 15 deletions packages/runtime-core/src/h.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ComponentOptions
} from './apiOptions'
import { ExtractPropTypes } from './componentProps'
import { currentRenderingInstance } from './componentRenderUtils'

// `h` is a more user-friendly version of `createVNode` that allows omitting the
// props when possible. It is intended for manually written render functions.
Expand Down Expand Up @@ -77,52 +78,52 @@ interface Constructor<P = any> {
// manually written render functions.

// element
export function h(type: string, children?: RawChildren): VNode
export function h(
function _h(type: string, children?: RawChildren): VNode
function _h(
type: string,
props?: RawProps | null,
children?: RawChildren
): VNode

// fragment
export function h(type: typeof Fragment, children?: VNodeArrayChildren): VNode
export function h(
function _h(type: typeof Fragment, children?: VNodeArrayChildren): VNode
function _h(
type: typeof Fragment,
props?: RawProps | null,
children?: VNodeArrayChildren
): VNode

// portal (target prop is required)
export function h(
function _h(
type: typeof Portal,
props: RawProps & PortalProps,
children: RawChildren
): VNode

// suspense
export function h(type: typeof Suspense, children?: RawChildren): VNode
export function h(
function _h(type: typeof Suspense, children?: RawChildren): VNode
function _h(
type: typeof Suspense,
props?: (RawProps & SuspenseProps) | null,
children?: RawChildren | RawSlots
): VNode

// functional component
export function h(type: FunctionalComponent, children?: RawChildren): VNode
export function h<P>(
function _h(type: FunctionalComponent, children?: RawChildren): VNode
function _h<P>(
type: FunctionalComponent<P>,
props?: (RawProps & P) | ({} extends P ? null : never),
children?: RawChildren | RawSlots
): VNode

// stateful component
export function h(type: ComponentOptions, children?: RawChildren): VNode
export function h(
function _h(type: ComponentOptions, children?: RawChildren): VNode
function _h(
type: ComponentOptionsWithoutProps | ComponentOptionsWithArrayProps,
props?: RawProps | null,
children?: RawChildren | RawSlots
): VNode
export function h<O>(
function _h<O>(
type: ComponentOptionsWithObjectProps<O>,
props?:
| (RawProps & ExtractPropTypes<O>)
Expand All @@ -131,15 +132,15 @@ export function h<O>(
): VNode

// fake constructor type returned by `defineComponent` or class component
export function h(type: Constructor, children?: RawChildren): VNode
export function h<P>(
function _h(type: Constructor, children?: RawChildren): VNode
function _h<P>(
type: Constructor<P>,
props?: (RawProps & P) | ({} extends P ? null : never),
children?: RawChildren | RawSlots
): VNode

// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
function _h(type: any, propsOrChildren?: any, children?: any): VNode {
if (arguments.length === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
Expand All @@ -159,3 +160,24 @@ export function h(type: any, propsOrChildren?: any, children?: any): VNode {
return createVNode(type, propsOrChildren, children)
}
}

export const h: typeof _h = __DEV__ ? (applyTransformedH as typeof _h) : _h

let argsTransformer: Function | undefined

// This is used to hook into the h function and transform its arguments
// Useful for re-implementing behavior that was previously done with createElement in Vue 2
function applyTransformedH(...args: unknown[]): VNode {
if (argsTransformer) {
args = argsTransformer(args, currentRenderingInstance)
}
return _h(...(args as Parameters<typeof _h>))
}

export function transformHArgs(transformer: Function): void {
argsTransformer = transformer
}

export function resetTransformHArgs(): void {
argsTransformer = undefined
}