diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index f63f50f7..b0f4d147 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -1,17 +1,344 @@ +import { + h, + nodeOps, + render, + serializeInner, + triggerEvent, + TestElement, + nextTick, + renderToString, + ref +} from '@vue/runtime-test' + describe('api: options', () => { - test('data', () => {}) + test('data', async () => { + const Comp = { + data() { + return { + foo: 1 + } + }, + render() { + return h( + 'div', + { + onClick: () => { + this.foo++ + } + }, + this.foo + ) + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
1
`) - test('computed', () => {}) + triggerEvent(root.children[0] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(`
2
`) + }) - test('methods', () => {}) + test('computed', async () => { + const Comp = { + data() { + return { + foo: 1 + } + }, + computed: { + bar() { + return this.foo + 1 + }, + baz() { + return this.bar + 1 + } + }, + render() { + return h( + 'div', + { + onClick: () => { + this.foo++ + } + }, + this.bar + this.baz + ) + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
5
`) - test('watch', () => {}) + triggerEvent(root.children[0] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(`
7
`) + }) - test('provide/inject', () => {}) + test('methods', async () => { + const Comp = { + data() { + return { + foo: 1 + } + }, + methods: { + inc() { + this.foo++ + } + }, + render() { + return h( + 'div', + { + onClick: this.inc + }, + this.foo + ) + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
1
`) - test('mixins', () => {}) + triggerEvent(root.children[0] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(`
2
`) + }) - test('extends', () => {}) + test('watch', async () => { + function returnThis() { + return this + } + const spyA = jest.fn(returnThis) + const spyB = jest.fn(returnThis) + const spyC = jest.fn(returnThis) + + let ctx: any + const Comp = { + data() { + return { + foo: 1, + bar: 2, + baz: { + qux: 3 + } + } + }, + watch: { + // string method name + foo: 'onFooChange', + // direct function + bar: spyB, + baz: { + handler: spyC, + deep: true + } + }, + methods: { + onFooChange: spyA + }, + render() { + ctx = this + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + + function assertCall(spy: jest.Mock, callIndex: number, args: any[]) { + expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args) + } + + assertCall(spyA, 0, [1, undefined]) + assertCall(spyB, 0, [2, undefined]) + assertCall(spyC, 0, [{ qux: 3 }, undefined]) + expect(spyA).toHaveReturnedWith(ctx) + expect(spyB).toHaveReturnedWith(ctx) + expect(spyC).toHaveReturnedWith(ctx) + + ctx.foo++ + await nextTick() + expect(spyA).toHaveBeenCalledTimes(2) + assertCall(spyA, 1, [2, 1]) + + ctx.bar++ + await nextTick() + expect(spyB).toHaveBeenCalledTimes(2) + assertCall(spyB, 1, [3, 2]) + + ctx.baz.qux++ + await nextTick() + expect(spyC).toHaveBeenCalledTimes(2) + // new and old objects have same identity + assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }]) + }) + + test('provide/inject', () => { + const Root = { + data() { + return { + a: 1 + } + }, + provide() { + return { + a: this.a + } + }, + render() { + return [h(ChildA), h(ChildB), h(ChildC), h(ChildD)] + } + } + const ChildA = { + inject: ['a'], + render() { + return this.a + } + } + const ChildB = { + // object alias + inject: { b: 'a' }, + render() { + return this.b + } + } + const ChildC = { + inject: { + b: { + from: 'a' + } + }, + render() { + return this.b + } + } + const ChildD = { + inject: { + b: { + from: 'c', + default: 2 + } + }, + render() { + return this.b + } + } + + expect(renderToString(h(Root))).toBe(`1112`) + }) test('lifecycle', () => {}) + + test('mixins', () => { + const calls: string[] = [] + const mixinA = { + data() { + return { + a: 1 + } + }, + mounted() { + calls.push('mixinA') + } + } + const mixinB = { + data() { + return { + b: 2 + } + }, + mounted() { + calls.push('mixinB') + } + } + const Comp = { + mixins: [mixinA, mixinB], + data() { + return { + c: 3 + } + }, + mounted() { + calls.push('comp') + }, + render() { + return `${this.a}${this.b}${this.c}` + } + } + + expect(renderToString(h(Comp))).toBe(`123`) + expect(calls).toEqual(['mixinA', 'mixinB', 'comp']) + }) + + test('extends', () => { + const calls: string[] = [] + const Base = { + data() { + return { + a: 1 + } + }, + mounted() { + calls.push('base') + } + } + const Comp = { + extends: Base, + data() { + return { + b: 2 + } + }, + mounted() { + calls.push('comp') + }, + render() { + return `${this.a}${this.b}` + } + } + + expect(renderToString(h(Comp))).toBe(`12`) + expect(calls).toEqual(['base', 'comp']) + }) + + test('accessing setup() state from options', async () => { + const Comp = { + setup() { + return { + count: ref(0) + } + }, + data() { + return { + plusOne: this.count + 1 + } + }, + computed: { + plusTwo() { + return this.count + 2 + } + }, + methods: { + inc() { + this.count++ + } + }, + render() { + return h( + 'div', + { + onClick: this.inc + }, + `${this.count},${this.plusOne},${this.plusTwo}` + ) + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
0,1,2
`) + + triggerEvent(root.children[0] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(`
1,1,3
`) + }) }) diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index dc3db1a7..6070b8c3 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -28,7 +28,7 @@ import { onBeforeUnmount, onUnmounted } from './apiLifecycle' -import { DebuggerEvent } from '@vue/reactivity' +import { DebuggerEvent, reactive } from '@vue/reactivity' import { warn } from './warning' // TODO legacy component definition also supports constructors with .options @@ -83,7 +83,7 @@ export function applyOptions( asMixin: boolean = false ) { const data = - instance.data === EMPTY_OBJ ? (instance.data = {}) : instance.data + instance.data === EMPTY_OBJ ? (instance.data = reactive({})) : instance.data const ctx = instance.renderProxy as any const { // composition @@ -135,7 +135,13 @@ export function applyOptions( } if (computedOptions) { for (const key in computedOptions) { - data[key] = computed(computedOptions[key] as any) + const opt = computedOptions[key] + data[key] = isFunction(opt) + ? computed(opt.bind(ctx)) + : computed({ + get: opt.get.bind(ctx), + set: opt.set.bind(ctx) + }) } } if (methods) { @@ -148,9 +154,9 @@ export function applyOptions( const raw = watchOptions[key] const getter = () => ctx[key] if (isString(raw)) { - const handler = data[key] + const handler = data[raw] if (isFunction(handler)) { - watch(getter, handler.bind(ctx)) + watch(getter, handler as any) } else if (__DEV__) { // TODO warn invalid watch handler path } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 48c35239..c3ac0b52 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -337,7 +337,7 @@ export function setupStatefulComponent(instance: ComponentInstance) { // setup returned bindings. // assuming a render function compiled from template is present. if (isObject(setupResult)) { - instance.data = setupResult + instance.data = reactive(setupResult) } else if (__DEV__ && setupResult !== undefined) { warn( `setup() should return an object. Received: ${ @@ -360,7 +360,9 @@ export function setupStatefulComponent(instance: ComponentInstance) { if (__FEATURE_OPTIONS__) { applyOptions(instance, Component) } - instance.data = reactive(instance.data === EMPTY_OBJ ? {} : instance.data) + if (instance.data === EMPTY_OBJ) { + instance.data = reactive({}) + } currentInstance = null }