From d435acbfb224dc2cbaff6412dd4f4dbc3c66b847 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Mon, 4 Mar 2024 23:36:17 +0900 Subject: [PATCH 01/33] feat(runtime-vapor): component slot --- packages/runtime-vapor/src/component.ts | 3 + packages/runtime-vapor/src/componentSlots.ts | 21 ++++ packages/runtime-vapor/src/index.ts | 1 + packages/runtime-vapor/src/render.ts | 7 +- packages/runtime-vapor/src/slot.ts | 6 ++ playground/src/main.ts | 2 +- playground/src/slots.js | 100 +++++++++++++++++++ 7 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 packages/runtime-vapor/src/componentSlots.ts create mode 100644 packages/runtime-vapor/src/slot.ts create mode 100644 playground/src/slots.js diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index fce0a9e0d..6b7c4ad16 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -15,6 +15,7 @@ import { emit, normalizeEmitsOptions, } from './componentEmits' +import type { InternalSlots } from './componentSlots' import type { Data } from '@vue/shared' import { VaporLifecycleHooks } from './enums' @@ -56,6 +57,7 @@ export interface ComponentInternalInstance { setupState: Data emit: EmitFn emitted: Record | null + slots: InternalSlots refs: Data vapor: true @@ -175,6 +177,7 @@ export const createComponentInstance = ( props: EMPTY_OBJ, attrs: EMPTY_OBJ, setupState: EMPTY_OBJ, + slots: EMPTY_OBJ, refs: EMPTY_OBJ, vapor: true, diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts new file mode 100644 index 000000000..73521af5b --- /dev/null +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -0,0 +1,21 @@ +import type { IfAny } from '@vue/shared' +import type { Block } from './render' +import type { ComponentInternalInstance } from './component' + +export type Slot = ( + ...args: IfAny +) => Block[] + +export type InternalSlots = { + [name: string]: Slot | undefined +} + +export type Slots = Readonly + +export const initSlots = ( + instance: ComponentInternalInstance, + slots: Slots, +) => { + // TODO: normalize? + instance.slots = slots +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 0cf0116b9..fa7a30b86 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -105,6 +105,7 @@ export { } from './apiLifecycle' export { createIf } from './if' export { createFor } from './for' +export { createSlots as createSlot } from './slot' // **Internal** DOM-only runtime directive helpers export { diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index e7425a4c3..fd8729503 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -14,6 +14,7 @@ import { unsetCurrentInstance, } from './component' import { initProps } from './componentProps' +import { type Slots, initSlots } from './componentSlots' import { invokeDirectiveHook } from './directives' import { insert, querySelector, remove } from './dom/element' import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler' @@ -30,10 +31,12 @@ export type Fragment = { export function render( comp: Component, props: Data, + slots: Slots, // TODO: container: string | ParentNode, ): ComponentInternalInstance { const instance = createComponentInstance(comp, props) initProps(instance, props, !isFunction(instance.component)) + initSlots(instance, slots) const component = mountComponent( instance, (container = normalizeContainer(container)), @@ -56,8 +59,8 @@ function mountComponent( const reset = setCurrentInstance(instance) const block = instance.scope.run(() => { - const { component, props, emit, attrs } = instance - const ctx = { expose: () => {}, emit, attrs } + const { component, props, emit, attrs, slots } = instance + const ctx = { expose: () => {}, emit, attrs, slots } const setupFn = isFunction(component) ? component : component.setup const stateOrNode = setupFn && setupFn(props, ctx) diff --git a/packages/runtime-vapor/src/slot.ts b/packages/runtime-vapor/src/slot.ts new file mode 100644 index 000000000..46db4c631 --- /dev/null +++ b/packages/runtime-vapor/src/slot.ts @@ -0,0 +1,6 @@ +import type { Slots } from './componentSlots' + +// TODO: intercept? +export const createSlots = (slots: Slots) => { + return slots +} diff --git a/playground/src/main.ts b/playground/src/main.ts index d962dae1f..3d672a377 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -7,7 +7,7 @@ const mod = (modules['.' + location.pathname] || modules['./App.vue'])() mod.then(({ default: mod }) => { if (mod.vapor) { - const instance = render(mod, {}, '#app') + const instance = render(mod, {}, {}, '#app') // @ts-expect-error globalThis.unmount = () => { unmountComponent(instance) diff --git a/playground/src/slots.js b/playground/src/slots.js new file mode 100644 index 000000000..d8afd49df --- /dev/null +++ b/playground/src/slots.js @@ -0,0 +1,100 @@ +// @ts-check +import { + children, + createSlot, + defineComponent, + getCurrentInstance, + insert, + on, + ref, + render as renderComponent, + renderEffect, + setText, + template, +} from '@vue/vapor' + +const t0 = template('
') + +// +const t1 = template( + '

', +) + +const Parent = defineComponent({ + vapor: true, + props: undefined, + setup(props) {}, + render(_ctx) { + const n0 = /** @type {any} */ (t0()) + const s0 = createSlot({ + mySlot: scope => { + const n1 = t1() + const n2 = /** @type {any} */ (children(n1, 0)) + const n3 = /** @type {any} */ (children(n1, 1)) + renderEffect(() => { + setText(n2, scope.message) + }) + on(n3, 'click', scope.changeMessage) + return [n1] + }, + // e.g. default slot + // default: () => { + // const n1 = t1() + // return [n1] + // } + }) + renderComponent(Child, {}, s0, n0) + return n0 + }, +}) + +const t2 = template( + '
', +) + +const Child = defineComponent({ + vapor: true, + props: undefined, + setup(props, { expose: __expose }) { + __expose() + const message = ref('Hello World!') + function changeMessage() { + message.value += '!' + } + const __returned__ = { message, changeMessage } + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true, + }) + return __returned__ + }, + render(_ctx) { + const instance = /** @type {any} */ (getCurrentInstance()) + const slots = instance.slots + + //
+ // + // + //
+ const n0 = /** @type {any} */ (t2()) + const n1 = /** @type {any} */ (children(n0, 0)) + on(n1, 'click', _ctx.changeMessage) + const mySlot = slots.mySlot({ + get message() { + return _ctx.message + }, + get changeMessage() { + return _ctx.changeMessage + }, + }) + insert(mySlot, n0, n1) + return n0 + }, +}) + +export default Parent From 61ada62d7e0d58f931fdfd449042ab1beadd264d Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Mon, 4 Mar 2024 23:46:07 +0900 Subject: [PATCH 02/33] chore: render fn arg --- packages/runtime-vapor/__tests__/_utils.ts | 4 +++- packages/runtime-vapor/__tests__/componentProps.spec.ts | 1 + playground/src/props.js | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index d4501273d..17adc4c84 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -6,6 +6,7 @@ import { render as _render, defineComponent, } from '../src' +import type { Slots } from '../src/componentSlots' export function makeRender( initHost = () => { @@ -28,9 +29,10 @@ export function makeRender( let instance: ComponentInternalInstance const render = ( props: Data = {}, + slots: Slots = {}, container: string | ParentNode = '#host', ) => { - instance = _render(component, props, container) + instance = _render(component, props, slots, container) return res() } const res = () => ({ diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts index df493d71d..72eff574c 100644 --- a/packages/runtime-vapor/__tests__/componentProps.spec.ts +++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts @@ -316,6 +316,7 @@ describe('component props (vapor)', () => { return _ctx.id }, }, + {}, n0 as HTMLDivElement, ) return n0 diff --git a/playground/src/props.js b/playground/src/props.js index 8da476cc8..10960ef90 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -46,8 +46,6 @@ export default defineComponent({ // insert(n0, c0) renderComponent( /** @type {any} */ (child), - - // TODO: proxy?? { /* */ get count() { @@ -59,6 +57,7 @@ export default defineComponent({ return _ctx.count * 2 }, }, + {}, // @ts-expect-error TODO n0[0], ) From f4768bc536b178254307eadebe0abeb093f2ff47 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Mon, 4 Mar 2024 23:48:39 +0900 Subject: [PATCH 03/33] chore playground --- playground/src/slots.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/src/slots.js b/playground/src/slots.js index d8afd49df..880ad57cb 100644 --- a/playground/src/slots.js +++ b/playground/src/slots.js @@ -75,7 +75,7 @@ const Child = defineComponent({ }, render(_ctx) { const instance = /** @type {any} */ (getCurrentInstance()) - const slots = instance.slots + const { slots } = instance //
// @@ -84,7 +84,7 @@ const Child = defineComponent({ const n0 = /** @type {any} */ (t2()) const n1 = /** @type {any} */ (children(n0, 0)) on(n1, 'click', _ctx.changeMessage) - const mySlot = slots.mySlot({ + const s0 = slots.mySlot({ get message() { return _ctx.message }, @@ -92,7 +92,7 @@ const Child = defineComponent({ return _ctx.changeMessage }, }) - insert(mySlot, n0, n1) + insert(s0, n0, n1) return n0 }, }) From ffee6720e72475eaeff9adf46a26ceb87317b256 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 10 Mar 2024 18:41:38 +0900 Subject: [PATCH 04/33] test(runtime-vapor): component slots (def test cases) --- .../__tests__/componentSlots.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/runtime-vapor/__tests__/componentSlots.spec.ts diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts new file mode 100644 index 000000000..5b1a7b9e4 --- /dev/null +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -0,0 +1,43 @@ +// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. + +import { makeRender } from './_utils' + +const define = makeRender() + +describe('component: slots', () => { + test.todo('initSlots: instance.slots should be set correctly', () => {}) + + test.todo( + 'initSlots: should normalize object slots (when value is null, string, array)', + () => {}, + ) + + test.todo( + 'initSlots: should normalize object slots (when value is function)', + () => {}, + ) + + test.todo( + 'initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', + () => {}, + ) + + test.todo( + 'updateSlots: instance.slots should be updated correctly (when slotType is number)', + async () => {}, + ) + + test.todo( + 'updateSlots: instance.slots should be updated correctly (when slotType is null)', + async () => {}, + ) + + test.todo( + 'updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', + async () => {}, + ) + + test.todo('should respect $stable flag', async () => {}) + + test.todo('should not warn when mounting another app in setup', () => {}) +}) From 246badc0769d931a7e9aa122fb261d1308a6149b Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Mon, 11 Mar 2024 00:04:13 +0900 Subject: [PATCH 05/33] test(runtime-vapor): component slots [wip] --- .../__tests__/componentSlots.spec.ts | 234 ++++++++++++++++-- 1 file changed, 220 insertions(+), 14 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 5b1a7b9e4..4c67102fa 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -1,43 +1,249 @@ // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. +import { + defineComponent, + getCurrentInstance, + nextTick, + ref, + render as renderComponent, + template, +} from '../src' +import { createSlots } from '../src/slot' import { makeRender } from './_utils' const define = makeRender() describe('component: slots', () => { - test.todo('initSlots: instance.slots should be set correctly', () => {}) + function renderWithSlots(slots: any): any { + let instance: any + const Comp = defineComponent({ + vapor: true, + render() { + const t0 = template('
') + const n0 = t0() + instance = getCurrentInstance() + return n0 + }, + }) + + const { render } = define({ + render() { + const t0 = template('
') + const n0 = t0() + renderComponent(Comp, {}, slots, n0 as ParentNode) + }, + }) + + render() + return instance + } + + test('initSlots: instance.slots should be set correctly', () => { + const { slots } = renderWithSlots({ _: 1 }) + expect(slots).toMatchObject({ _: 1 }) + }) test.todo( 'initSlots: should normalize object slots (when value is null, string, array)', - () => {}, + () => { + // TODO: normalize + }, ) test.todo( 'initSlots: should normalize object slots (when value is function)', - () => {}, + () => { + // TODO: normalize + }, ) - test.todo( - 'initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', - () => {}, - ) + test('initSlots: instance.slots should be set correctly', () => { + let proxy: any + const { render } = define({ + render() { + const t0 = template('
') + const n0 = t0() + proxy = getCurrentInstance() + return n0 + }, + }) + + render( + {}, + createSlots({ + header: () => { + const t0 = template('header') + // TODO: single node + return [t0()] + }, + }), + ) + expect(proxy.slots.header()).toMatchObject([ + document.createTextNode('header'), + ]) + }) + test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => { + const { slots } = renderWithSlots( + createSlots({ + // TODO: normalize from array + default: () => { + const t0 = template('') + return [t0()] + }, + }), + ) + + // TODO: warn + // expect( + // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.', + // ).toHaveBeenWarned() + + expect(slots.default()).toMatchObject([document.createElement('span')]) + }) + + // TODO: dynamic slot test.todo( - 'updateSlots: instance.slots should be updated correctly (when slotType is number)', - async () => {}, + 'updateSlots: instance.slots should be updated correctly', + async () => { + const flag1 = ref(true) + + let instance: any + const Child = () => { + instance = getCurrentInstance() + return template('child')() + } + + const { render } = define({ + render() { + const t0 = template('
') + const n0 = t0() + renderComponent( + Child, + {}, + createSlots({ + default: () => { + // TODO: dynamic slot + return flag1.value + ? [template('')()] + : [template('
')()] + }, + }), + n0 as ParentNode, + ) + return [] + }, + }) + + render() + + expect(instance.slots.default()).toMatchObject([]) + expect(instance.slots.default()).not.toMatchObject([]) + + flag1.value = false + await nextTick() + + expect(instance.slots.default()).not.toMatchObject([]) + expect(instance.slots.default()).toMatchObject([]) + }, ) + // TODO: dynamic slots test.todo( - 'updateSlots: instance.slots should be updated correctly (when slotType is null)', - async () => {}, + 'updateSlots: instance.slots should be updated correctly', + async () => { + const flag1 = ref(true) + + let instance: any + const Child = () => { + instance = getCurrentInstance() + return template('child')() + } + + const oldSlots = { + header: () => template('header')(), + footer: undefined, + } + const newSlots = { + header: undefined, + footer: () => template('footer')(), + } + + const { render } = define({ + render() { + const t0 = template('
') + const n0 = t0() + // renderComponent( + // Child, + // {}, + // createSlots(flag1.value ? oldSlots : newSlots), + // n0 as ParentNode, + // ) + return [] + }, + }) + + render() + + expect(instance.slots).toMatchObject({ _: null }) + + flag1.value = false + await nextTick() + + expect(instance.slots).toMatchObject({ _: null }) + }, ) test.todo( 'updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', - async () => {}, + async () => { + // TODO: dynamic slots + }, ) - test.todo('should respect $stable flag', async () => {}) + test.todo('should respect $stable flag', async () => { + // TODO: $stable flag + }) - test.todo('should not warn when mounting another app in setup', () => {}) + test.todo('should not warn when mounting another app in setup', () => { + // TODO: warning and createApp fn + // const Comp = { + // render() { + // const i = getCurrentInstance() + // return i?.slots.default?.() + // }, + // } + // const mountComp = () => { + // createApp({ + // render() { + // const t0 = template('
') + // const n0 = t0() + // renderComponent( + // Comp, + // {}, + // createSlots({ + // default: () => { + // const t0 = template('msg') + // return [t0()] + // }, + // }), + // n0, + // ) + // return n0 + // }, + // }) + // } + // const App = { + // setup() { + // mountComp() + // }, + // render() { + // return null + // }, + // } + // createApp(App).mount(document.createElement('div')) + // expect( + // 'Slot "default" invoked outside of the render function', + // ).not.toHaveBeenWarned() + }) }) From 60edb08528d1747b36e48fe669217f5d3d1eea6d Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 17 Mar 2024 16:19:05 +0900 Subject: [PATCH 06/33] chore: add comment --- packages/runtime-vapor/src/componentSlots.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 73521af5b..f2164d2a2 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -19,3 +19,5 @@ export const initSlots = ( // TODO: normalize? instance.slots = slots } + +// TODO: $stable ? From d6903315a844659b77e772bccdbdca8090e9dbdf Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 17 Mar 2024 17:07:41 +0900 Subject: [PATCH 07/33] chore: merge refactoring of component --- .../runtime-vapor/src/apiCreateComponent.ts | 9 +- packages/runtime-vapor/src/component.ts | 5 +- packages/runtime-vapor/src/componentSlots.ts | 5 +- playground/src/slots.js | 99 +++++++++---------- 4 files changed, 58 insertions(+), 60 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateComponent.ts b/packages/runtime-vapor/src/apiCreateComponent.ts index c74783b32..4d40f0e66 100644 --- a/packages/runtime-vapor/src/apiCreateComponent.ts +++ b/packages/runtime-vapor/src/apiCreateComponent.ts @@ -5,10 +5,15 @@ import { } from './component' import { setupComponent } from './apiRender' import type { RawProps } from './componentProps' +import type { Slots } from './componentSlots' -export function createComponent(comp: Component, rawProps: RawProps = null) { +export function createComponent( + comp: Component, + rawProps: RawProps = null, + slots: Slots | null = null, +) { const current = currentInstance! - const instance = createComponentInstance(comp, rawProps) + const instance = createComponentInstance(comp, rawProps, slots) setupComponent(instance) // register sub-component with current component for lifecycle management diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 9aa512bf3..54c755754 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -17,7 +17,7 @@ import { emit, normalizeEmitsOptions, } from './componentEmits' -import type { InternalSlots } from './componentSlots' +import { type InternalSlots, type Slots, initSlots } from './componentSlots' import { VaporLifecycleHooks } from './apiLifecycle' import type { Data } from '@vue/shared' @@ -142,6 +142,7 @@ let uid = 0 export function createComponentInstance( component: ObjectComponent | FunctionalComponent, rawProps: RawProps | null, + slots: Slots | null, ): ComponentInternalInstance { const instance: ComponentInternalInstance = { uid: uid++, @@ -228,7 +229,7 @@ export function createComponentInstance( // [VaporLifecycleHooks.SERVER_PREFETCH]: null, } initProps(instance, rawProps, !isFunction(component)) - // TODO: initSlots(instance, slots) + initSlots(instance, slots) instance.emit = emit.bind(null, instance) return instance diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index f2164d2a2..5bacab2f0 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,6 +1,6 @@ import type { IfAny } from '@vue/shared' -import type { Block } from './render' import type { ComponentInternalInstance } from './component' +import type { Block } from './apiRender' export type Slot = ( ...args: IfAny @@ -14,8 +14,9 @@ export type Slots = Readonly export const initSlots = ( instance: ComponentInternalInstance, - slots: Slots, + slots: Slots | null, ) => { + if (!slots) slots = {} // TODO: normalize? instance.slots = slots } diff --git a/playground/src/slots.js b/playground/src/slots.js index 880ad57cb..2cd6982b1 100644 --- a/playground/src/slots.js +++ b/playground/src/slots.js @@ -1,13 +1,13 @@ // @ts-check import { children, - createSlot, + createComponent, + createSlots, defineComponent, getCurrentInstance, insert, on, ref, - render as renderComponent, renderEffect, setText, template, @@ -27,29 +27,31 @@ const t1 = template( const Parent = defineComponent({ vapor: true, - props: undefined, - setup(props) {}, - render(_ctx) { - const n0 = /** @type {any} */ (t0()) - const s0 = createSlot({ - mySlot: scope => { - const n1 = t1() - const n2 = /** @type {any} */ (children(n1, 0)) - const n3 = /** @type {any} */ (children(n1, 1)) - renderEffect(() => { - setText(n2, scope.message) - }) - on(n3, 'click', scope.changeMessage) - return [n1] - }, - // e.g. default slot - // default: () => { - // const n1 = t1() - // return [n1] - // } - }) - renderComponent(Child, {}, s0, n0) - return n0 + + setup(props) { + return (() => { + const n0 = /** @type {any} */ (t0()) + const s0 = createSlots({ + mySlot: scope => { + const n1 = t1() + const n2 = /** @type {any} */ (children(n1, 0)) + const n3 = /** @type {any} */ (children(n1, 1)) + renderEffect(() => { + setText(n2, scope.message()) + }) + on(n3, 'click', scope.changeMessage) + return [n1] + }, + // e.g. default slot + // default: () => { + // const n1 = t1() + // return [n1] + // } + }) + /** @type {any} */ + const n1 = createComponent(Child, {}, s0) + return [n0, n1] + })() }, }) @@ -59,41 +61,30 @@ const t2 = template( const Child = defineComponent({ vapor: true, - props: undefined, - setup(props, { expose: __expose }) { + setup(_, { expose: __expose }) { __expose() const message = ref('Hello World!') function changeMessage() { message.value += '!' } - const __returned__ = { message, changeMessage } - Object.defineProperty(__returned__, '__isScriptSetup', { - enumerable: false, - value: true, - }) - return __returned__ - }, - render(_ctx) { - const instance = /** @type {any} */ (getCurrentInstance()) - const { slots } = instance - //
- // - // - //
- const n0 = /** @type {any} */ (t2()) - const n1 = /** @type {any} */ (children(n0, 0)) - on(n1, 'click', _ctx.changeMessage) - const s0 = slots.mySlot({ - get message() { - return _ctx.message - }, - get changeMessage() { - return _ctx.changeMessage - }, - }) - insert(s0, n0, n1) - return n0 + return (() => { + const instance = /** @type {any} */ (getCurrentInstance()) + const { slots } = instance + //
+ // + // + //
+ const n0 = /** @type {any} */ (t2()) + const n1 = /** @type {any} */ (children(n0, 0)) + on(n1, 'click', () => changeMessage) + const s0 = slots.mySlot({ + message: () => message.value, + changeMessage: () => changeMessage, + }) + insert(s0, n0, n1) + return n0 + })() }, }) From e967ed17de1635065d607d75cae83d13a2fabf69 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 17 Mar 2024 17:13:48 +0900 Subject: [PATCH 08/33] chore: fix test --- packages/runtime-vapor/__tests__/_utils.ts | 2 +- .../__tests__/componentSlots.spec.ts | 18 ++++++------------ .../runtime-vapor/src/apiCreateVaporApp.ts | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index 0fa8d92bb..befb3dff3 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -34,7 +34,7 @@ export function makeRender( slots: Slots = {}, container: string | ParentNode = '#host', ) => { - app = createVaporApp(component, props) + app = createVaporApp(component, props, slots) instance = app.mount(container) return res() diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 4c67102fa..625af8684 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -1,11 +1,11 @@ // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. import { + createComponent, defineComponent, getCurrentInstance, nextTick, ref, - render as renderComponent, template, } from '../src' import { createSlots } from '../src/slot' @@ -28,9 +28,7 @@ describe('component: slots', () => { const { render } = define({ render() { - const t0 = template('
') - const n0 = t0() - renderComponent(Comp, {}, slots, n0 as ParentNode) + return createComponent(Comp, {}, slots) }, }) @@ -58,12 +56,12 @@ describe('component: slots', () => { ) test('initSlots: instance.slots should be set correctly', () => { - let proxy: any + let instance: any const { render } = define({ render() { const t0 = template('
') const n0 = t0() - proxy = getCurrentInstance() + instance = getCurrentInstance() return n0 }, }) @@ -78,7 +76,7 @@ describe('component: slots', () => { }, }), ) - expect(proxy.slots.header()).toMatchObject([ + expect(instance.slots.header()).toMatchObject([ document.createTextNode('header'), ]) }) @@ -116,9 +114,7 @@ describe('component: slots', () => { const { render } = define({ render() { - const t0 = template('
') - const n0 = t0() - renderComponent( + return createComponent( Child, {}, createSlots({ @@ -129,9 +125,7 @@ describe('component: slots', () => { : [template('
')()] }, }), - n0 as ParentNode, ) - return [] }, }) diff --git a/packages/runtime-vapor/src/apiCreateVaporApp.ts b/packages/runtime-vapor/src/apiCreateVaporApp.ts index 31dacf356..084514fb6 100644 --- a/packages/runtime-vapor/src/apiCreateVaporApp.ts +++ b/packages/runtime-vapor/src/apiCreateVaporApp.ts @@ -40,7 +40,7 @@ export function createVaporApp( mount(rootContainer): any { if (!instance) { - instance = createComponentInstance(rootComponent, rootProps) + instance = createComponentInstance(rootComponent, rootProps, rootSlots) setupComponent(instance) render(instance, rootContainer) return instance From fd2d9ff092c3795f750581d66bf02929643ef864 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 17 Mar 2024 19:25:14 +0900 Subject: [PATCH 09/33] feat(runtime-vapor): dynamic slots --- .../__tests__/componentSlots.spec.ts | 221 +++++++++--------- packages/runtime-vapor/src/apiCreateSlots.ts | 65 ++++++ packages/runtime-vapor/src/componentSlots.ts | 6 +- packages/runtime-vapor/src/index.ts | 2 +- packages/runtime-vapor/src/slot.ts | 6 - 5 files changed, 182 insertions(+), 118 deletions(-) create mode 100644 packages/runtime-vapor/src/apiCreateSlots.ts delete mode 100644 packages/runtime-vapor/src/slot.ts diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 625af8684..8484482cf 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -2,13 +2,14 @@ import { createComponent, + createVaporApp, defineComponent, getCurrentInstance, nextTick, ref, template, } from '../src' -import { createSlots } from '../src/slot' +import { createSlots } from '../src/apiCreateSlots' import { makeRender } from './_utils' const define = makeRender() @@ -69,80 +70,65 @@ describe('component: slots', () => { render( {}, createSlots({ - header: () => { - const t0 = template('header') - // TODO: single node - return [t0()] - }, + header: () => template('header')(), }), ) - expect(instance.slots.header()).toMatchObject([ + expect(instance.slots.header()).toMatchObject( document.createTextNode('header'), - ]) + ) }) + // TODO: test case name test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => { const { slots } = renderWithSlots( createSlots({ - // TODO: normalize from array - default: () => { - const t0 = template('') - return [t0()] - }, + // TODO: normalize from array? + default: () => template('')(), }), ) - // TODO: warn // expect( // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.', // ).toHaveBeenWarned() - expect(slots.default()).toMatchObject([document.createElement('span')]) + expect(slots.default()).toMatchObject(document.createElement('span')) }) - // TODO: dynamic slot - test.todo( - 'updateSlots: instance.slots should be updated correctly', - async () => { - const flag1 = ref(true) + test('updateSlots: instance.slots should be updated correctly', async () => { + const flag1 = ref(true) - let instance: any - const Child = () => { - instance = getCurrentInstance() - return template('child')() - } + let instance: any + const Child = () => { + instance = getCurrentInstance() + return template('child')() + } - const { render } = define({ - render() { - return createComponent( - Child, - {}, - createSlots({ - default: () => { - // TODO: dynamic slot - return flag1.value - ? [template('')()] - : [template('
')()] - }, - }), - ) - }, - }) + const { render } = define({ + render() { + return createComponent( + Child, + {}, + createSlots({ _: 2 as any }, () => [ + flag1.value + ? { name: 'one', fn: () => template('')() } + : { name: 'two', fn: () => template('
')() }, + ]), + ) + }, + }) - render() + render() - expect(instance.slots.default()).toMatchObject([]) - expect(instance.slots.default()).not.toMatchObject([]) + expect(instance.slots).toHaveProperty('one') + expect(instance.slots).not.toHaveProperty('two') - flag1.value = false - await nextTick() + flag1.value = false + await nextTick() - expect(instance.slots.default()).not.toMatchObject([]) - expect(instance.slots.default()).toMatchObject([]) - }, - ) + expect(instance.slots).not.toHaveProperty('one') + expect(instance.slots).toHaveProperty('two') + }) - // TODO: dynamic slots test.todo( 'updateSlots: instance.slots should be updated correctly', async () => { @@ -164,16 +150,15 @@ describe('component: slots', () => { } const { render } = define({ - render() { - const t0 = template('
') - const n0 = t0() - // renderComponent( - // Child, - // {}, - // createSlots(flag1.value ? oldSlots : newSlots), - // n0 as ParentNode, - // ) - return [] + setup() { + return (() => { + return createComponent( + Child, + {}, + // TODO: maybe it is not supported + createSlots(flag1.value ? oldSlots : newSlots), + ) + })() }, }) @@ -188,56 +173,78 @@ describe('component: slots', () => { }, ) - test.todo( - 'updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', - async () => { - // TODO: dynamic slots - }, - ) + // TODO: test case name + test('updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', async () => { + const flag1 = ref(true) + + let instance: any + const Child = () => { + instance = getCurrentInstance() + return template('child')() + } + + const { render } = define({ + setup() { + return createComponent( + Child, + {}, + createSlots({}, () => [ + flag1.value + ? [{ name: 'header', fn: () => template('header')() }] + : [{ name: 'footer', fn: () => template('footer')() }], + ]), + ) + }, + }) + render() + + expect(instance.slots).toHaveProperty('header') + flag1.value = false + await nextTick() + + // expect( + // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.', + // ).toHaveBeenWarned() + + expect(instance.slots).toHaveProperty('footer') + }) test.todo('should respect $stable flag', async () => { - // TODO: $stable flag + // TODO: $stable flag? }) test.todo('should not warn when mounting another app in setup', () => { - // TODO: warning and createApp fn - // const Comp = { - // render() { - // const i = getCurrentInstance() - // return i?.slots.default?.() - // }, - // } - // const mountComp = () => { - // createApp({ - // render() { - // const t0 = template('
') - // const n0 = t0() - // renderComponent( - // Comp, - // {}, - // createSlots({ - // default: () => { - // const t0 = template('msg') - // return [t0()] - // }, - // }), - // n0, - // ) - // return n0 - // }, - // }) - // } - // const App = { - // setup() { - // mountComp() - // }, - // render() { - // return null - // }, - // } - // createApp(App).mount(document.createElement('div')) - // expect( - // 'Slot "default" invoked outside of the render function', - // ).not.toHaveBeenWarned() + // TODO: warning + const Comp = defineComponent({ + render() { + const i = getCurrentInstance() + return i!.slots.default!() + }, + }) + const mountComp = () => { + createVaporApp({ + render() { + return createComponent( + Comp, + {}, + createSlots({ + default: () => template('msg')(), + }), + )! + }, + }) + } + const App = { + setup() { + mountComp() + }, + render() { + return null! + }, + } + createVaporApp(App).mount(document.createElement('div')) + expect( + 'Slot "default" invoked outside of the render function', + ).not.toHaveBeenWarned() }) }) diff --git a/packages/runtime-vapor/src/apiCreateSlots.ts b/packages/runtime-vapor/src/apiCreateSlots.ts new file mode 100644 index 000000000..af619da0a --- /dev/null +++ b/packages/runtime-vapor/src/apiCreateSlots.ts @@ -0,0 +1,65 @@ +// NOTE: this filed is based on `runtime-core/src/helpers/createSlots.ts` + +import { EMPTY_ARR, isArray } from '@vue/shared' +import { renderWatch } from './renderWatch' +import type { InternalSlots, Slot } from './componentSlots' + +// TODO: SSR + +interface CompiledSlotDescriptor { + name: string + fn: Slot + key?: string +} + +export const createSlots = ( + slots: InternalSlots, + dynamicSlotsGetter?: () => ( + | CompiledSlotDescriptor + | CompiledSlotDescriptor[] + | undefined + )[], +): InternalSlots => { + const dynamicSlotKeys: Record = {} + renderWatch( + () => dynamicSlotsGetter?.() ?? EMPTY_ARR, + dynamicSlots => { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i] + // array of dynamic slot generated by