diff --git a/packages/runtime-core/__tests__/apiSetupContext.spec.ts b/packages/runtime-core/__tests__/apiSetupContext.spec.ts index 46b65e3a9..796347b7c 100644 --- a/packages/runtime-core/__tests__/apiSetupContext.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupContext.spec.ts @@ -1,5 +1,174 @@ +import { ref, reactive } from '@vue/reactivity' +import { + renderToString, + h, + nodeOps, + render, + serializeInner, + nextTick, + watch, + createComponent, + triggerEvent, + TestElement +} from '@vue/runtime-test' + // reference: https://vue-composition-api-rfc.netlify.com/api.html#setup describe('api: setup context', () => { - test.todo('should work') + it('should expose return values to template render context', () => { + const Comp = { + setup() { + return { + // ref should auto-unwrap + ref: ref('foo'), + // object exposed as-is + object: reactive({ msg: 'bar' }), + // primitive value exposed as-is + value: 'baz' + } + }, + render() { + return `${this.ref} ${this.object.msg} ${this.value}` + } + } + expect(renderToString(h(Comp))).toMatch(`foo bar baz`) + }) + + it('should support returning render function', () => { + const Comp = { + setup() { + return () => { + return h('div', 'hello') + } + } + } + expect(renderToString(h(Comp))).toMatch(`hello`) + }) + + it('props', async () => { + const count = ref(0) + let dummy + + const Parent = { + render: () => h(Child, { count: count.value }) + } + + const Child = createComponent({ + setup(props: { count: number }) { + watch(() => { + dummy = props.count + }) + return () => h('div', props.count) + } + }) + + const root = nodeOps.createElement('div') + render(h(Parent), root) + expect(serializeInner(root)).toMatch(`
0
`) + expect(dummy).toBe(0) + + // props should be reactive + count.value++ + await nextTick() + expect(serializeInner(root)).toMatch(`
1
`) + expect(dummy).toBe(1) + }) + + it('context.attrs', async () => { + const toggle = ref(true) + + const Parent = { + render: () => h(Child, toggle.value ? { id: 'foo' } : { class: 'baz' }) + } + + const Child = { + // explicit empty props declaration + // puts everything received in attrs + props: {}, + setup(props: any, { attrs }: any) { + return () => h('div', attrs) + } + } + + const root = nodeOps.createElement('div') + render(h(Parent), root) + expect(serializeInner(root)).toMatch(`
`) + + // should update even though it's not reactive + toggle.value = false + await nextTick() + expect(serializeInner(root)).toMatch(`
`) + }) + + it('context.slots', async () => { + const id = ref('foo') + + const Parent = { + render: () => + h(Child, null, { + foo: () => id.value, + bar: () => 'bar' + }) + } + + const Child = { + setup(props: any, { slots }: any) { + return () => h('div', [...slots.foo(), ...slots.bar()]) + } + } + + const root = nodeOps.createElement('div') + render(h(Parent), root) + expect(serializeInner(root)).toMatch(`
foobar
`) + + // should update even though it's not reactive + id.value = 'baz' + await nextTick() + expect(serializeInner(root)).toMatch(`
bazbar
`) + }) + + it('context.emit', async () => { + const count = ref(0) + const spy = jest.fn() + + const Parent = { + render: () => + h(Child, { + count: count.value, + onInc: (newVal: number) => { + spy() + count.value = newVal + } + }) + } + + const Child = createComponent({ + props: { + count: { + type: Number, + default: 1 + } + }, + setup(props, { emit }) { + return () => + h( + 'div', + { + onClick: () => emit('inc', props.count + 1) + }, + props.count + ) + } + }) + + const root = nodeOps.createElement('div') + render(h(Parent), root) + expect(serializeInner(root)).toMatch(`
0
`) + + // emit should trigger parent handler + triggerEvent(root.children[0] as TestElement, 'click') + expect(spy).toHaveBeenCalled() + await nextTick() + expect(serializeInner(root)).toMatch(`
1
`) + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index ad363a3bf..21e5b443d 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -92,8 +92,6 @@ interface SetupContext { attrs: Data slots: Slots refs: Data - parent: ComponentInstance | null - root: ComponentInstance emit: ((event: string, ...args: unknown[]) => void) } @@ -288,9 +286,7 @@ function createSetupContext(instance: ComponentInstance): SetupContext { attrs: new Proxy(instance, SetupProxyHandlers.attrs), slots: new Proxy(instance, SetupProxyHandlers.slots), refs: new Proxy(instance, SetupProxyHandlers.refs), - emit: instance.emit, - parent: instance.parent, - root: instance.root + emit: instance.emit } as any return __DEV__ ? Object.freeze(context) : context } @@ -305,9 +301,7 @@ export function renderComponentRoot(instance: ComponentInstance): VNode { slots, attrs, refs, - emit, - parent, - root + emit } = instance if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { return normalizeVNode( @@ -322,9 +316,7 @@ export function renderComponentRoot(instance: ComponentInstance): VNode { attrs, slots, refs, - emit, - parent, - root + emit }) : render(props, null as any) ) @@ -387,5 +379,6 @@ function hasPropsChanged(prevProps: Data, nextProps: Data): boolean { return true } } + console.log(111) return false } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 792657425..745fcc8e1 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -184,7 +184,7 @@ export function resolveProps( instance.props = __DEV__ ? readonly(props) : props instance.attrs = options - ? __DEV__ + ? __DEV__ && attrs != null ? readonly(attrs) : attrs : instance.props diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts index 8dd9c4376..e9e363744 100644 --- a/packages/runtime-dom/src/modules/props.ts +++ b/packages/runtime-dom/src/modules/props.ts @@ -12,5 +12,5 @@ export function patchDOMProp( if ((key === 'innerHTML' || key === 'textContent') && prevChildren != null) { unmountChildren(prevChildren, parentComponent) } - el[key] = value + el[key] = value == null ? '' : value } diff --git a/packages/runtime-test/src/index.ts b/packages/runtime-test/src/index.ts index 4c211fd9f..9e1816f5a 100644 --- a/packages/runtime-test/src/index.ts +++ b/packages/runtime-test/src/index.ts @@ -1,14 +1,22 @@ import { createRenderer, VNode } from '@vue/runtime-core' import { nodeOps, TestElement } from './nodeOps' import { patchProp } from './patchProp' +import { serializeInner } from './serialize' export const render = createRenderer({ patchProp, ...nodeOps }) as (node: VNode | null, container: TestElement) => VNode -export { serialize } from './serialize' -export { triggerEvent } from './triggerEvent' +// convenience for one-off render validations +export function renderToString(vnode: VNode) { + const root = nodeOps.createElement('div') + render(vnode, root) + return serializeInner(root) +} + +export * from './triggerEvent' +export * from './serialize' export * from './nodeOps' export * from './jestUtils' export * from '@vue/runtime-core' diff --git a/packages/runtime-test/src/serialize.ts b/packages/runtime-test/src/serialize.ts index fa401f4ad..72ed03d2e 100644 --- a/packages/runtime-test/src/serialize.ts +++ b/packages/runtime-test/src/serialize.ts @@ -19,6 +19,19 @@ export function serialize( } } +export function serializeInner( + node: TestElement, + indent: number = 0, + depth: number = 0 +) { + const newLine = indent ? `\n` : `` + return node.children.length + ? newLine + + node.children.map(c => serialize(c, indent, depth + 1)).join(newLine) + + newLine + : `` +} + function serializeElement( node: TestElement, indent: number, @@ -26,19 +39,15 @@ function serializeElement( ): string { const props = Object.keys(node.props) .map(key => { - return isOn(key) ? `` : `${key}=${JSON.stringify(node.props[key])}` + const value = node.props[key] + return isOn(key) || value == null ? `` : `${key}=${JSON.stringify(value)}` }) + .filter(_ => _) .join(' ') - const newLine = indent ? `\n` : `` - const children = node.children.length - ? newLine + - node.children.map(c => serialize(c, indent, depth + 1)).join(newLine) + - newLine - : `` const padding = indent ? ` `.repeat(indent).repeat(depth) : `` return ( `${padding}<${node.tag}${props ? ` ${props}` : ``}>` + - `${children}` + + `${serializeInner(node, indent, depth)}` + `${padding}` ) }