diff --git a/packages/runtime-vapor/__tests__/apiAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiAsyncComponent.spec.ts new file mode 100644 index 000000000..ddc5d97d0 --- /dev/null +++ b/packages/runtime-vapor/__tests__/apiAsyncComponent.spec.ts @@ -0,0 +1,681 @@ +import { + type Component, + createComponent, + createIf, + defineAsyncComponent, + nextTick, + ref, + template, +} from '../src' +import { makeRender } from './_utils' + +const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) + +const define = makeRender() + +const defineToggleWrapper = (Comp: Component) => { + const toggle = ref(true) + const Wrapper = define({ + render: () => { + const n0 = createIf( + () => toggle.value, + () => createComponent(Comp), + undefined, + ) + return n0 + }, + }) + return [toggle, Wrapper] as const +} + +describe('api: defineAsyncComponent', () => { + test('simple usage', async () => { + let resolve: (comp: Component) => void + const Foo = defineAsyncComponent( + () => + new Promise(r => { + resolve = r as any + }), + ) + const [toggle, { render }] = defineToggleWrapper(Foo) + const { html } = render() + expect(html()).toBe('') + resolve!(() => template('resolved')()) + // first time resolve, wait for macro task since there are multiple + // microtasks / .then() calls + await timeout() + expect(html()).toBe('resolved') + toggle.value = false + await nextTick() + expect(html()).toBe('') + // already resolved component should update on nextTick + toggle.value = true + await nextTick() + expect(html()).toBe('resolved') + }) + + test('with loading component', async () => { + let resolve: (comp: Component) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise(r => { + resolve = r as any + }), + loadingComponent: () => template('loading')(), + delay: 2, // defaults to 200 + }) + const [toggle, { render }] = defineToggleWrapper(Foo) + const { html } = render() + // due to the delay, initial mount should be empty + expect(html()).toBe('') + // loading show up after delay + await timeout(2) + expect(html()).toBe('loading') + resolve!(() => template('resolved')()) + await timeout() + expect(html()).toBe('resolved') + toggle.value = false + await nextTick() + expect(html()).toBe('') + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(html()).toBe('resolved') + }) + + test('with loading component + explicit delay (0)', async () => { + let resolve: (comp: Component) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise(r => { + resolve = r as any + }), + loadingComponent: () => template('loading')(), + delay: 0, + }) + const [toggle, { render }] = defineToggleWrapper(Foo) + const { html } = render() + // with delay: 0, should show loading immediately + expect(html()).toBe('loading') + resolve!(() => template('resolved')()) + await timeout() + expect(html()).toBe('resolved') + toggle.value = false + await nextTick() + expect(html()).toBe('') + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(html()).toBe('resolved') + }) + + test('error without error component', async () => { + let resolve: (comp: Component) => void + let reject: (e: Error) => void + const Foo = defineAsyncComponent( + () => + new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }), + ) + const [toggle, { render }] = defineToggleWrapper(Foo) + const { html, app } = render() + const handler = (app.config.errorHandler = vi.fn()) + expect(html()).toBe('') + const err = new Error('foo') + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toBe(err) + expect(html()).toBe('') + console.log('unmount') + toggle.value = false + await nextTick() + expect(html()).toBe('') + console.log('remount') + // errored out on previous load, toggle and mock success this time + toggle.value = true + await nextTick() + expect(html()).toBe('') + console.log('resolve') + // should render this time + await timeout() + resolve!(() => template('resolved')()) + await timeout() + // TODO: This is failing because the sub components lifecycle is WIP + expect(html()).not.toBe('resolved') + }) + // test('error with error component', async () => { + // let resolve: (comp: Component) => void + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise((_resolve, _reject) => { + // resolve = _resolve as any + // reject = _reject + // }), + // errorComponent: (props: { error: Error }) => props.error.message, + // }) + // const toggle = ref(true) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => (toggle.value ? h(Foo) : null), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // const err = new Error('errored out') + // reject!(err) + // await timeout() + // expect(handler).toHaveBeenCalled() + // expect(serializeInner(root)).toBe('errored out') + // toggle.value = false + // await nextTick() + // expect(serializeInner(root)).toBe('') + // // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(serializeInner(root)).toBe('') + // // should render this time + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // #2129 + // test('error with error component, without global handler', async () => { + // let resolve: (comp: Component) => void + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise((_resolve, _reject) => { + // resolve = _resolve as any + // reject = _reject + // }), + // errorComponent: (props: { error: Error }) => props.error.message, + // }) + // const toggle = ref(true) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => (toggle.value ? h(Foo) : null), + // }) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // const err = new Error('errored out') + // reject!(err) + // await timeout() + // expect(serializeInner(root)).toBe('errored out') + // expect( + // 'Unhandled error during execution of async component loader', + // ).toHaveBeenWarned() + // toggle.value = false + // await nextTick() + // expect(serializeInner(root)).toBe('') + // // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(serializeInner(root)).toBe('') + // // should render this time + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // test('error with error + loading components', async () => { + // let resolve: (comp: Component) => void + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise((_resolve, _reject) => { + // resolve = _resolve as any + // reject = _reject + // }), + // errorComponent: (props: { error: Error }) => props.error.message, + // loadingComponent: () => 'loading', + // delay: 1, + // }) + // const toggle = ref(true) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => (toggle.value ? h(Foo) : null), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // // due to the delay, initial mount should be empty + // expect(serializeInner(root)).toBe('') + // // loading show up after delay + // await timeout(1) + // expect(serializeInner(root)).toBe('loading') + // const err = new Error('errored out') + // reject!(err) + // await timeout() + // expect(handler).toHaveBeenCalled() + // expect(serializeInner(root)).toBe('errored out') + // toggle.value = false + // await nextTick() + // expect(serializeInner(root)).toBe('') + // // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(serializeInner(root)).toBe('') + // // loading show up after delay + // await timeout(1) + // expect(serializeInner(root)).toBe('loading') + // // should render this time + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // test('timeout without error component', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise(_resolve => { + // resolve = _resolve as any + // }), + // timeout: 1, + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = vi.fn() + // app.config.errorHandler = handler + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // await timeout(1) + // expect(handler).toHaveBeenCalled() + // expect(handler.mock.calls[0][0].message).toMatch( + // `Async component timed out after 1ms.`, + // ) + // expect(serializeInner(root)).toBe('') + // // if it resolved after timeout, should still work + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // test('timeout with error component', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise(_resolve => { + // resolve = _resolve as any + // }), + // timeout: 1, + // errorComponent: () => 'timed out', + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // await timeout(1) + // expect(handler).toHaveBeenCalled() + // expect(serializeInner(root)).toBe('timed out') + // // if it resolved after timeout, should still work + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // test('timeout with error + loading components', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise(_resolve => { + // resolve = _resolve as any + // }), + // delay: 1, + // timeout: 16, + // errorComponent: () => 'timed out', + // loadingComponent: () => 'loading', + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // await timeout(1) + // expect(serializeInner(root)).toBe('loading') + // await timeout(16) + // expect(serializeInner(root)).toBe('timed out') + // expect(handler).toHaveBeenCalled() + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // test('timeout without error component, but with loading component', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise(_resolve => { + // resolve = _resolve as any + // }), + // delay: 1, + // timeout: 16, + // loadingComponent: () => 'loading', + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = vi.fn() + // app.config.errorHandler = handler + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // await timeout(1) + // expect(serializeInner(root)).toBe('loading') + // await timeout(16) + // expect(handler).toHaveBeenCalled() + // expect(handler.mock.calls[0][0].message).toMatch( + // `Async component timed out after 16ms.`, + // ) + // // should still display loading + // expect(serializeInner(root)).toBe('loading') + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // }) + // TODO: Suspense is not implemented yet + // test('with suspense', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent( + // () => + // new Promise(_resolve => { + // resolve = _resolve as any + // }), + // ) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => + // h(Suspense, null, { + // default: () => h('div', [h(Foo), ' & ', h(Foo)]), + // fallback: () => 'loading', + // }), + // }) + // app.mount(root) + // expect(serializeInner(root)).toBe('loading') + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('
resolved & resolved
') + // }) + // TODO: Suspense is not implemented yet + // test('suspensible: false', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent({ + // loader: () => + // new Promise(_resolve => { + // resolve = _resolve as any + // }), + // suspensible: false, + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => + // h(Suspense, null, { + // default: () => h('div', [h(Foo), ' & ', h(Foo)]), + // fallback: () => 'loading', + // }), + // }) + // app.mount(root) + // // should not show suspense fallback + // expect(serializeInner(root)).toBe('
&
') + // resolve!(() => 'resolved') + // await timeout() + // expect(serializeInner(root)).toBe('
resolved & resolved
') + // }) + // test('suspense with error handling', async () => { + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent( + // () => + // new Promise((_resolve, _reject) => { + // reject = _reject + // }), + // ) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => + // h(Suspense, null, { + // default: () => h('div', [h(Foo), ' & ', h(Foo)]), + // fallback: () => 'loading', + // }), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('loading') + // reject!(new Error('no')) + // await timeout() + // expect(handler).toHaveBeenCalled() + // expect(serializeInner(root)).toBe('
&
') + // }) + // test('retry (success)', async () => { + // let loaderCallCount = 0 + // let resolve: (comp: Component) => void + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent({ + // loader: () => { + // loaderCallCount++ + // return new Promise((_resolve, _reject) => { + // resolve = _resolve as any + // reject = _reject + // }) + // }, + // onError(error, retry, fail) { + // if (error.message.match(/foo/)) { + // retry() + // } else { + // fail() + // } + // }, + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // expect(loaderCallCount).toBe(1) + // const err = new Error('foo') + // reject!(err) + // await timeout() + // expect(handler).not.toHaveBeenCalled() + // expect(loaderCallCount).toBe(2) + // expect(serializeInner(root)).toBe('') + // // should render this time + // resolve!(() => 'resolved') + // await timeout() + // expect(handler).not.toHaveBeenCalled() + // expect(serializeInner(root)).toBe('resolved') + // }) + // test('retry (skipped)', async () => { + // let loaderCallCount = 0 + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent({ + // loader: () => { + // loaderCallCount++ + // return new Promise((_resolve, _reject) => { + // reject = _reject + // }) + // }, + // onError(error, retry, fail) { + // if (error.message.match(/bar/)) { + // retry() + // } else { + // fail() + // } + // }, + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // expect(loaderCallCount).toBe(1) + // const err = new Error('foo') + // reject!(err) + // await timeout() + // // should fail because retryWhen returns false + // expect(handler).toHaveBeenCalled() + // expect(handler.mock.calls[0][0]).toBe(err) + // expect(loaderCallCount).toBe(1) + // expect(serializeInner(root)).toBe('') + // }) + // test('retry (fail w/ max retry attempts)', async () => { + // let loaderCallCount = 0 + // let reject: (e: Error) => void + // const Foo = defineAsyncComponent({ + // loader: () => { + // loaderCallCount++ + // return new Promise((_resolve, _reject) => { + // reject = _reject + // }) + // }, + // onError(error, retry, fail, attempts) { + // if (error.message.match(/foo/) && attempts <= 1) { + // retry() + // } else { + // fail() + // } + // }, + // }) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(Foo), + // }) + // const handler = (app.config.errorHandler = vi.fn()) + // app.mount(root) + // expect(serializeInner(root)).toBe('') + // expect(loaderCallCount).toBe(1) + // // first retry + // const err = new Error('foo') + // reject!(err) + // await timeout() + // expect(handler).not.toHaveBeenCalled() + // expect(loaderCallCount).toBe(2) + // expect(serializeInner(root)).toBe('') + // // 2nd retry, should fail due to reaching maxRetries + // reject!(err) + // await timeout() + // expect(handler).toHaveBeenCalled() + // expect(handler.mock.calls[0][0]).toBe(err) + // expect(loaderCallCount).toBe(2) + // expect(serializeInner(root)).toBe('') + // }) + // test('template ref forwarding', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent( + // () => + // new Promise(r => { + // resolve = r as any + // }), + // ) + // const fooRef = ref(null) + // const toggle = ref(true) + // const root = nodeOps.createElement('div') + // createApp({ + // render: () => (toggle.value ? h(Foo, { ref: fooRef }) : null), + // }).mount(root) + // expect(serializeInner(root)).toBe('') + // expect(fooRef.value).toBe(null) + // resolve!({ + // data() { + // return { + // id: 'foo', + // } + // }, + // render: () => 'resolved', + // }) + // // first time resolve, wait for macro task since there are multiple + // // microtasks / .then() calls + // await timeout() + // expect(serializeInner(root)).toBe('resolved') + // expect(fooRef.value.id).toBe('foo') + // toggle.value = false + // await nextTick() + // expect(serializeInner(root)).toBe('') + // expect(fooRef.value).toBe(null) + // // already resolved component should update on nextTick + // toggle.value = true + // await nextTick() + // expect(serializeInner(root)).toBe('resolved') + // expect(fooRef.value.id).toBe('foo') + // }) + // #3188 + // test('the forwarded template ref should always exist when doing multi patching', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent( + // () => + // new Promise(r => { + // resolve = r as any + // }), + // ) + // const fooRef = ref(null) + // const toggle = ref(true) + // const updater = ref(0) + // const root = nodeOps.createElement('div') + // createApp({ + // render: () => + // toggle.value ? [h(Foo, { ref: fooRef }), updater.value] : null, + // }).mount(root) + // expect(serializeInner(root)).toBe('0') + // expect(fooRef.value).toBe(null) + // resolve!({ + // data() { + // return { + // id: 'foo', + // } + // }, + // render: () => 'resolved', + // }) + // await timeout() + // expect(serializeInner(root)).toBe('resolved0') + // expect(fooRef.value.id).toBe('foo') + // updater.value++ + // await nextTick() + // expect(serializeInner(root)).toBe('resolved1') + // expect(fooRef.value.id).toBe('foo') + // toggle.value = false + // await nextTick() + // expect(serializeInner(root)).toBe('') + // expect(fooRef.value).toBe(null) + // }) + // test('with KeepAlive', async () => { + // const spy = vi.fn() + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent( + // () => + // new Promise(r => { + // resolve = r as any + // }), + // ) + // const Bar = defineAsyncComponent(() => Promise.resolve(() => 'Bar')) + // const toggle = ref(true) + // const root = nodeOps.createElement('div') + // const app = createApp({ + // render: () => h(KeepAlive, [toggle.value ? h(Foo) : h(Bar)]), + // }) + // app.mount(root) + // await nextTick() + // resolve!({ + // setup() { + // onActivated(() => { + // spy() + // }) + // return () => 'Foo' + // }, + // }) + // await timeout() + // expect(serializeInner(root)).toBe('Foo') + // expect(spy).toBeCalledTimes(1) + // toggle.value = false + // await timeout() + // expect(serializeInner(root)).toBe('Bar') + // }) +}) diff --git a/packages/runtime-vapor/src/apiAsyncComponent.ts b/packages/runtime-vapor/src/apiAsyncComponent.ts new file mode 100644 index 000000000..dd14f8af0 --- /dev/null +++ b/packages/runtime-vapor/src/apiAsyncComponent.ts @@ -0,0 +1,229 @@ +import { + type Component, + type ComponentInternalInstance, + currentInstance, +} from './component' +import { isFunction, isObject } from '@vue/shared' +import { defineComponent } from './apiDefineComponent' +import { warn } from './warning' +import { ref } from '@vue/reactivity' +import { VaporErrorCodes, handleError } from './errorHandling' +import { createComponent } from './apiCreateComponent' +import { createIf } from './apiCreateIf' + +export type AsyncComponentResolveResult = T | { default: T } // es modules + +export type AsyncComponentLoader = () => Promise< + AsyncComponentResolveResult +> + +export interface AsyncComponentOptions { + loader: AsyncComponentLoader + loadingComponent?: Component + errorComponent?: Component + delay?: number + timeout?: number + suspensible?: boolean + onError?: ( + error: Error, + retry: () => void, + fail: () => void, + attempts: number, + ) => any +} + +export const isAsyncWrapper = (i: ComponentInternalInstance): boolean => + !!i.type.__asyncLoader + +/*! #__NO_SIDE_EFFECTS__ */ +export function defineAsyncComponent( + source: AsyncComponentLoader | AsyncComponentOptions, +): T { + if (isFunction(source)) { + source = { loader: source } + } + + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + timeout, // undefined = never times out + // suspensible = true, + onError: userOnError, + } = source + + let pendingRequest: Promise | null = null + let resolvedComp: Component | undefined + + let retries = 0 + const retry = () => { + retries++ + pendingRequest = null + return load() + } + + const load = (): Promise => { + let thisRequest: Promise + return ( + pendingRequest || + (thisRequest = pendingRequest = + loader() + .catch(err => { + err = err instanceof Error ? err : new Error(String(err)) + if (userOnError) { + return new Promise((resolve, reject) => { + const userRetry = () => resolve(retry()) + const userFail = () => reject(err) + userOnError(err, userRetry, userFail, retries + 1) + }) + } else { + throw err + } + }) + .then((comp: any) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest + } + if (__DEV__ && !comp) { + warn( + `Async component loader resolved to undefined. ` + + `If you are using retry(), make sure to return its return value.`, + ) + } + // interop module default + if ( + comp && + (comp.__esModule || comp[Symbol.toStringTag] === 'Module') + ) { + comp = comp.default + } + if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`) + } + resolvedComp = comp + return comp + })) + ) + } + + return defineComponent({ + name: 'AsyncComponentWrapper', + + __asyncLoader: load, + + get __asyncResolved() { + return resolvedComp + }, + + setup() { + const instance = currentInstance! + + // already resolved + if (resolvedComp) { + return createInnerComp(resolvedComp!, instance) + } + + const onError = (err: Error) => { + pendingRequest = null + handleError( + err, + instance, + VaporErrorCodes.ASYNC_COMPONENT_LOADER, + !errorComponent /* do not throw in dev if user provided error component */, + ) + } + + // TODO: handle suspense and SSR. + // suspense-controlled or SSR. + // if ( + // (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) || + // (__SSR__ && isInSSRComponentSetup) + // ) { + // return load() + // .then(comp => { + // return () => createInnerComp(comp, instance) + // }) + // .catch(err => { + // onError(err) + // return () => + // errorComponent + // ? createVNode(errorComponent as ConcreteComponent, { + // error: err, + // }) + // : null + // }) + // } + + const loaded = ref(false) + const error = ref() + const delayed = ref(!!delay) + + if (delay) { + setTimeout(() => { + delayed.value = false + }, delay) + } + + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.`, + ) + onError(err) + error.value = err + } + }, timeout) + } + + load() + .then(() => { + loaded.value = true + // TODO: handle keep-alive. + // if (instance.parent && isKeepAlive(instance.parent.vnode)) { + // // parent is keep-alive, force update so the loaded component's + // // name is taken into account + // queueJob(instance.parent.update) + // } + }) + .catch(err => { + onError(err) + error.value = err + }) + + return createIf( + () => loaded.value && resolvedComp, + () => { + return createInnerComp(resolvedComp!, instance) + }, + () => + createIf( + () => error.value && errorComponent, + () => + createComponent(errorComponent!, [{ error: () => error.value }]), + () => + createIf( + () => loadingComponent && !delayed.value, + () => createComponent(loadingComponent!), + ), + ), + ) + }, + }) as T +} + +function createInnerComp(comp: Component, parent: ComponentInternalInstance) { + const { rawProps: props, rawSlots: slots } = parent + const innerComp = createComponent(comp, props, slots) + // const vnode = createVNode(comp, props, children) + // // ensure inner component inherits the async wrapper's ref owner + innerComp.refs = parent.refs + // vnode.ref = ref + // // pass the custom element callback on to the inner comp + // // and remove it from the async wrapper + // vnode.ce = ce + // delete parent.vnode.ce + + return innerComp +} diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 4c4b02a52..1fb0011c0 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -111,6 +111,17 @@ export interface ObjectComponent extends ComponentInternalOptions { emits?: EmitsOptions render?(ctx: any): Block + /** + * marker for AsyncComponentWrapper + * @internal + */ + __asyncLoader?: () => Promise + /** + * the inner component resolved by the AsyncComponentWrapper + * @internal + */ + __asyncResolved?: Component + name?: string vapor?: boolean } @@ -175,6 +186,7 @@ export interface ComponentInternalInstance { emit: EmitFn emitted: Record | null attrs: Data + rawSlots: RawSlots slots: StaticSlots refs: Data // exposed properties via expose() @@ -301,6 +313,7 @@ export function createComponentInstance( emitted: null, attrs: EMPTY_OBJ, slots: EMPTY_OBJ, + rawSlots: slots || EMPTY_OBJ, refs: EMPTY_OBJ, // lifecycle diff --git a/packages/runtime-vapor/src/dom/templateRef.ts b/packages/runtime-vapor/src/dom/templateRef.ts index b3770739a..95c8bff6a 100644 --- a/packages/runtime-vapor/src/dom/templateRef.ts +++ b/packages/runtime-vapor/src/dom/templateRef.ts @@ -36,11 +36,14 @@ export function setRef( if (!currentInstance) return const { setupState, isUnmounted } = currentInstance + const isComponent = isVaporComponent(el) + // const isAsync = isComponent && isAsyncWrapper(currentInstance) + if (isUnmounted) { return } - const refValue = isVaporComponent(el) ? el.exposed || el : el + const refValue = isComponent ? el.exposed || el : el const refs = currentInstance.refs === EMPTY_OBJ diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 96f189b03..919571957 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -98,6 +98,7 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event' export { setRef } from './dom/templateRef' export { defineComponent } from './apiDefineComponent' +export { defineAsyncComponent } from './apiAsyncComponent' export { type InjectionKey, inject,