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}${node.tag}>`
)
}