Skip to content

Commit

Permalink
feat: add defineAsyncComponent API (#644)
Browse files Browse the repository at this point in the history
  • Loading branch information
mathe42 committed Feb 18, 2021
1 parent 69ca912 commit 8409f48
Show file tree
Hide file tree
Showing 4 changed files with 856 additions and 0 deletions.
126 changes: 126 additions & 0 deletions src/component/defineAsyncComponent.ts
@@ -0,0 +1,126 @@
import { isFunction, isObject, warn } from '../utils'
import { VueProxy } from './componentProxy'
import { AsyncComponent } from 'vue'

import {
ComponentOptionsWithoutProps,
ComponentOptionsWithArrayProps,
ComponentOptionsWithProps,
} from './componentOptions'

type ComponentOptions =
| ComponentOptionsWithoutProps
| ComponentOptionsWithArrayProps
| ComponentOptionsWithProps

type Component = VueProxy<any, any, any, any, any>

type ComponentOrComponentOptions = ComponentOptions | Component

export type AsyncComponentResolveResult<T = ComponentOrComponentOptions> =
| T
| { default: T } // es modules

export type AsyncComponentLoader = () => Promise<AsyncComponentResolveResult>

export interface AsyncComponentOptions {
loader: AsyncComponentLoader
loadingComponent?: ComponentOrComponentOptions
errorComponent?: ComponentOrComponentOptions
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}

export function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): AsyncComponent {
if (isFunction(source)) {
source = { loader: source }
}

const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout, // undefined = never times out
suspensible = false, // in Vue 3 default is true
onError: userOnError,
} = source

if (__DEV__ && suspensible) {
warn(
`The suspensiblbe option for async components is not supported in Vue2. It is ignored.`
)
}

let pendingRequest: Promise<Component> | null = null

let retries = 0
const retry = () => {
retries++
pendingRequest = null
return load()
}

const load = (): Promise<ComponentOrComponentOptions> => {
let thisRequest: Promise<ComponentOrComponentOptions>
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}`)
}
return comp
}))
)
}

return () => {
const component = load()

return {
component: component as any, // there is a type missmatch between vue2 type and the docs
delay,
timeout,
error: errorComponent,
loading: loadingComponent,
}
}
}
1 change: 1 addition & 0 deletions src/component/index.ts
@@ -1,4 +1,5 @@
export { defineComponent } from './defineComponent'
export { defineAsyncComponent } from './defineAsyncComponent'
export { SetupFunction, SetupContext } from './componentOptions'
export { ComponentInstance, ComponentRenderProxy } from './componentProxy'
export { Data } from './common'
Expand Down
74 changes: 74 additions & 0 deletions test-dts/defineAsyncComponent.test-d.ts
@@ -0,0 +1,74 @@
import { AsyncComponent } from 'vue'
import { defineAsyncComponent, defineComponent, expectType } from './index'

function asyncComponent1() {
return Promise.resolve().then(() => {
return defineComponent({})
})
}

function asyncComponent2() {
return Promise.resolve().then(() => {
return {
template: 'ASYNC',
}
})
}

const syncComponent1 = defineComponent({
template: '',
})

const syncComponent2 = {
template: '',
}

defineAsyncComponent(asyncComponent1)
defineAsyncComponent(asyncComponent2)

defineAsyncComponent({
loader: asyncComponent1,
delay: 200,
timeout: 3000,
errorComponent: syncComponent1,
loadingComponent: syncComponent1,
})

defineAsyncComponent({
loader: asyncComponent2,
delay: 200,
timeout: 3000,
errorComponent: syncComponent2,
loadingComponent: syncComponent2,
})

defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve(syncComponent1)
})
)

defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve(syncComponent2)
})
)

const component = defineAsyncComponent({
loader: asyncComponent1,
loadingComponent: defineComponent({}),
errorComponent: defineComponent({}),
delay: 200,
timeout: 3000,
suspensible: false,
onError(error, retry, fail, attempts) {
expectType<() => void>(retry)
expectType<() => void>(fail)
expectType<number>(attempts)
expectType<Error>(error)
},
})

expectType<AsyncComponent>(component)

0 comments on commit 8409f48

Please sign in to comment.