Skip to content

Commit 3b8cfc2

Browse files
committed
fix: revolve context.slots before setup
1 parent 62b2dbd commit 3b8cfc2

File tree

6 files changed

+211
-33
lines changed

6 files changed

+211
-33
lines changed

src/component/component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type VueProxy<PropsOptions, RawBindings> = Vue2ComponentOptions<
4242

4343
export interface SetupContext {
4444
readonly attrs: Record<string, string>;
45-
readonly slots: { [key: string]: VNode[] | undefined };
45+
readonly slots: { [key: string]: (...args: any[]) => VNode[] };
4646
readonly parent: ComponentInstance | null;
4747
readonly root: ComponentInstance;
4848

src/helper.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import Vue, { ComponentOptions, VueConstructor } from 'vue';
1+
import Vue, { VNode, ComponentOptions, VueConstructor } from 'vue';
22
import { ComponentInstance } from './component';
33
import { currentVue, getCurrentVM } from './runtimeContext';
4-
import { assert } from './utils';
4+
import { assert, warn } from './utils';
55

66
export function ensureCurrentVMInFn(hook: string): ComponentInstance {
77
const vm = getCurrentVM();
@@ -25,3 +25,42 @@ export function createComponentInstance<V extends Vue = Vue>(
2525
export function isComponentInstance(obj: any) {
2626
return currentVue && obj instanceof currentVue;
2727
}
28+
29+
export function createSlotProxy(vm: ComponentInstance, slotName: string) {
30+
return (...args: any) => {
31+
if (!vm.$scopedSlots[slotName]) {
32+
return warn(`slots.${slotName}() got called outside of the "render()" scope`, vm);
33+
}
34+
35+
return vm.$scopedSlots[slotName]!.apply(vm, args);
36+
};
37+
}
38+
39+
export function resolveSlots(
40+
slots: { [key: string]: Function } | void,
41+
normalSlots: { [key: string]: VNode[] | undefined }
42+
): { [key: string]: true } {
43+
let res: { [key: string]: true };
44+
if (!slots) {
45+
res = {};
46+
} else if (slots._normalized) {
47+
// fast path 1: child component re-render only, parent did not change
48+
return slots._normalized as any;
49+
} else {
50+
res = {};
51+
for (const key in slots) {
52+
if (slots[key] && key[0] !== '$') {
53+
res[key] = true;
54+
}
55+
}
56+
}
57+
58+
// expose normal slots on scopedSlots
59+
for (const key in normalSlots) {
60+
if (!(key in res)) {
61+
res[key] = true;
62+
}
63+
}
64+
65+
return res;
66+
}

src/setup.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { VueConstructor } from 'vue';
22
import { ComponentInstance, SetupContext, SetupFunction, Data } from './component';
33
import { Ref, isRef, isReactive, nonReactive } from './reactivity';
44
import { getCurrentVM, setCurrentVM } from './runtimeContext';
5+
import { resolveSlots, createSlotProxy } from './helper';
56
import { hasOwn, isPlainObject, assert, proxy, warn, isFunction } from './utils';
67
import { ref } from './apis/state';
78
import vmStateManager from './vmStateManager';
@@ -63,6 +64,31 @@ function updateTemplateRef(vm: ComponentInstance) {
6364
vmStateManager.set(vm, 'refs', validNewKeys);
6465
}
6566

67+
function resolveScopedSlots(vm: ComponentInstance, slotsProxy: { [x: string]: Function }): void {
68+
const parentVode = (vm.$options as any)._parentVnode;
69+
if (!parentVode) return;
70+
71+
const prevSlots = vmStateManager.get(vm, 'slots') || [];
72+
const curSlots = resolveSlots(parentVode.data.scopedSlots, vm.$slots);
73+
// remove staled slots
74+
for (let index = 0; index < prevSlots.length; index++) {
75+
const key = prevSlots[index];
76+
if (!curSlots[key]) {
77+
delete slotsProxy[key];
78+
}
79+
}
80+
81+
// proxy fresh slots
82+
const slotNames = Object.keys(curSlots);
83+
for (let index = 0; index < slotNames.length; index++) {
84+
const key = slotNames[index];
85+
if (!slotsProxy[key]) {
86+
slotsProxy[key] = createSlotProxy(vm, key);
87+
}
88+
}
89+
vmStateManager.set(vm, 'slots', slotNames);
90+
}
91+
6692
function activateCurrentInstance(
6793
vm: ComponentInstance,
6894
fn: (vm_: ComponentInstance) => any,
@@ -134,21 +160,26 @@ export function mixin(Vue: VueConstructor) {
134160
function initSetup(vm: ComponentInstance, props: Record<any, any> = {}) {
135161
const setup = vm.$options.setup!;
136162
const ctx = createSetupContext(vm);
163+
164+
// resolve scopedSlots and slots to functions
165+
resolveScopedSlots(vm, ctx.slots);
166+
137167
let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null;
138168
activateCurrentInstance(vm, () => {
139169
binding = setup(props, ctx);
140170
});
141171

142172
if (!binding) return;
143-
144173
if (isFunction(binding)) {
145174
// keep typescript happy with the binding type.
146175
const bindingFunc = binding;
147176
// keep currentInstance accessible for createElement
148-
vm.$options.render = () => activateCurrentInstance(vm, vm_ => bindingFunc(vm_.$props, ctx));
177+
vm.$options.render = () => {
178+
resolveScopedSlots(vm, ctx.slots);
179+
return activateCurrentInstance(vm, vm_ => bindingFunc(vm_.$props, ctx));
180+
};
149181
return;
150182
}
151-
152183
if (isPlainObject(binding)) {
153184
const bindingObj = binding;
154185
vmStateManager.set(vm, 'rawBindings', binding);
@@ -179,14 +210,10 @@ export function mixin(Vue: VueConstructor) {
179210
}
180211

181212
function createSetupContext(vm: ComponentInstance & { [x: string]: any }): SetupContext {
182-
const ctx = {} as SetupContext;
183-
const props: Array<string | [string, string]> = [
184-
'root',
185-
'parent',
186-
'refs',
187-
['slots', 'scopedSlots'],
188-
'attrs',
189-
];
213+
const ctx = {
214+
slots: {},
215+
} as SetupContext;
216+
const props: Array<string | [string, string]> = ['root', 'parent', 'refs', 'attrs'];
190217
const methodReturnVoid = ['emit'];
191218
props.forEach(key => {
192219
let targetKey: string;

src/vmStateManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentInstance, Data } from './component';
33
export interface VfaState {
44
refs?: string[];
55
rawBindings?: Data;
6+
slots?: string[];
67
}
78

89
function set<K extends keyof VfaState>(vm: ComponentInstance, key: K, value: VfaState[K]): void {

test/setup.spec.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,25 +112,6 @@ describe('setup', () => {
112112
expect(props.a).toBe(1);
113113
});
114114

115-
it('should receive SetupContext second params', () => {
116-
let context;
117-
const vm = new Vue({
118-
setup(_, ctx) {
119-
context = ctx;
120-
},
121-
});
122-
expect(context).toBeDefined();
123-
expect('parent' in context).toBe(true);
124-
expect(context.root).toBe(vm.$root);
125-
expect(context.parent).toBe(vm.$parent);
126-
expect(context.slots).toBe(vm.$scopedSlots);
127-
expect(context.attrs).toBe(vm.$attrs);
128-
129-
// CAUTION: this will be removed in 3.0
130-
expect(context.refs).toBe(vm.$refs);
131-
expect(typeof context.emit === 'function').toBe(true);
132-
});
133-
134115
it('warn for existing props', () => {
135116
new Vue({
136117
props: {

test/setupContext.spec.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const Vue = require('vue/dist/vue.common.js');
2+
const { ref, watch, createElement: h } = require('../src');
3+
4+
describe('setupContext', () => {
5+
it('should have proper properties', () => {
6+
let context;
7+
const vm = new Vue({
8+
setup(_, ctx) {
9+
context = ctx;
10+
},
11+
});
12+
expect(context).toBeDefined();
13+
expect('parent' in context).toBe(true);
14+
expect(context.root).toBe(vm.$root);
15+
expect(context.parent).toBe(vm.$parent);
16+
expect(context.slots).toBeDefined();
17+
expect(context.attrs).toBe(vm.$attrs);
18+
19+
// CAUTION: this will be removed in 3.0
20+
expect(context.refs).toBe(vm.$refs);
21+
expect(typeof context.emit === 'function').toBe(true);
22+
});
23+
24+
it('slots should work in render function', () => {
25+
const vm = new Vue({
26+
template: `
27+
<test>
28+
<template slot="default">
29+
<span>foo</span>
30+
</template>
31+
<template slot="item" slot-scope="props">
32+
<span>{{ props.text || 'meh' }}</span>
33+
</template>
34+
</test>
35+
`,
36+
components: {
37+
test: {
38+
setup(_, { slots }) {
39+
return () => {
40+
return h('div', [slots.default(), slots.item()]);
41+
};
42+
},
43+
},
44+
},
45+
}).$mount();
46+
expect(vm.$el.innerHTML).toBe('<span>foo</span><span>meh</span>');
47+
});
48+
49+
it('warn for slots calls outside of the render() function', () => {
50+
warn = jest.spyOn(global.console, 'error').mockImplementation(() => null);
51+
52+
new Vue({
53+
template: `
54+
<test>
55+
<template slot="default">
56+
<span>foo</span>
57+
</template>
58+
</test>
59+
`,
60+
components: {
61+
test: {
62+
setup(_, { slots }) {
63+
slots.default();
64+
},
65+
},
66+
},
67+
}).$mount();
68+
expect(warn.mock.calls[0][0]).toMatch(
69+
'slots.default() got called outside of the "render()" scope'
70+
);
71+
warn.mockRestore();
72+
});
73+
74+
it('staled slots should be removed', () => {
75+
const Child = {
76+
template: '<div><slot value="foo"/></div>',
77+
};
78+
const vm = new Vue({
79+
components: { Child },
80+
template: `
81+
<child>
82+
<template slot-scope="{ value }" v-if="value">
83+
foo {{ value }}
84+
</template>
85+
</child>
86+
`,
87+
}).$mount();
88+
expect(vm.$el.textContent).toMatch(`foo foo`);
89+
});
90+
91+
it('slots should be synchronized', done => {
92+
let slotKeys;
93+
const Foo = {
94+
setup(_, { slots }) {
95+
slotKeys = Object.keys(slots);
96+
return () => {
97+
slotKeys = Object.keys(slots);
98+
return h('div', [
99+
slots.default && slots.default('from foo default'),
100+
slots.one && slots.one('from foo one'),
101+
slots.two && slots.two('from foo two'),
102+
slots.three && slots.three('from foo three'),
103+
]);
104+
};
105+
},
106+
};
107+
108+
const vm = new Vue({
109+
data: {
110+
a: 'one',
111+
b: 'two',
112+
},
113+
template: `
114+
<foo>
115+
<template #[a]="one">a {{ one }} </template>
116+
<template v-slot:[b]="two">b {{ two }} </template>
117+
</foo>
118+
`,
119+
components: { Foo },
120+
}).$mount();
121+
expect(slotKeys).toEqual(['one', 'two']);
122+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`a from foo one b from foo two`);
123+
vm.a = 'two';
124+
vm.b = 'three';
125+
waitForUpdate(() => {
126+
// expect(slotKeys).toEqual(['one', 'three']);
127+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`a from foo two b from foo three `);
128+
}).then(done);
129+
});
130+
});

0 commit comments

Comments
 (0)