mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat: add hydration emit for callbacks
This commit is contained in:
parent
373e223268
commit
c0844b902e
@ -11,10 +11,12 @@ export const createLazyIOComponent = (loader: AsyncComponentLoader) => {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnVisible(props.hydrate as IntersectionObserverInit | undefined) })
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -29,14 +31,16 @@ export const createLazyNetworkComponent = (loader: AsyncComponentLoader) => {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
if (props.hydrate === 0) {
|
||||
const comp = defineAsyncComponent(loader)
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
}
|
||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnIdle(props.hydrate) })
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -52,11 +56,13 @@ export const createLazyEventComponent = (loader: AsyncComponentLoader) => {
|
||||
default: 'mouseover',
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
// @ts-expect-error Cannot type HTMLElementEventMap in props
|
||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnInteraction(props.hydrate) })
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -72,10 +78,12 @@ export const createLazyMediaComponent = (loader: AsyncComponentLoader) => {
|
||||
default: '(min-width: 1px)',
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnMediaQuery(props.hydrate) })
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -91,18 +99,20 @@ export const createLazyIfComponent = (loader: AsyncComponentLoader) => {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
if (props.hydrate) {
|
||||
const comp = defineAsyncComponent(loader)
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
}
|
||||
const strategy: HydrationStrategy = (hydrate) => {
|
||||
const unwatch = watch(() => props.hydrate, () => hydrate(), { once: true })
|
||||
return () => unwatch()
|
||||
}
|
||||
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -118,10 +128,12 @@ export const createLazyTimeComponent = (loader: AsyncComponentLoader) => {
|
||||
default: 2000,
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
if (props.hydrate === 0) {
|
||||
const comp = defineAsyncComponent(loader)
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
}
|
||||
const strategy: HydrationStrategy = (hydrate) => {
|
||||
const id = setTimeout(hydrate, props.hydrate)
|
||||
@ -129,7 +141,7 @@ export const createLazyTimeComponent = (loader: AsyncComponentLoader) => {
|
||||
}
|
||||
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -144,11 +156,13 @@ export const createLazyPromiseComponent = (loader: AsyncComponentLoader) => {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup (props, { attrs }) {
|
||||
emits: ['hydrated'],
|
||||
setup (props, { attrs, emit }) {
|
||||
const hydrated = () => { emit('hydrated') }
|
||||
if (!props.hydrate || typeof props.hydrate.then !== 'function') {
|
||||
const comp = defineAsyncComponent(loader)
|
||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
}
|
||||
const strategy: HydrationStrategy = (hydrate) => {
|
||||
// @ts-expect-error TS does not see hydrate as non-null
|
||||
@ -156,7 +170,7 @@ export const createLazyPromiseComponent = (loader: AsyncComponentLoader) => {
|
||||
return () => {}
|
||||
}
|
||||
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -115,21 +115,22 @@ export const componentsTypeTemplate = {
|
||||
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,
|
||||
]
|
||||
})
|
||||
|
||||
const islandType = 'type IslandComponent<T extends DefineComponent> = T & DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>>'
|
||||
const delayedType = 'type DelayedComponent<T> = DefineComponent<{hydrate?: T},{},{},{},{},{},{},{hydrated: void}>'
|
||||
return `
|
||||
import type { DefineComponent, SlotsType } from 'vue'
|
||||
${nuxt.options.experimental.componentIslands ? islandType : ''}
|
||||
${nuxt.options.experimental.delayedHydration ? delayedType : ''}
|
||||
interface _GlobalComponents {
|
||||
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyIdle${pascalName}': ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyTime${pascalName}': ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyPromise${pascalName}': ${type} & DefineComponent<{hydrate?: Promise<unknown>}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyVisible${pascalName}': ${type} & DefineComponent<{hydrate?: Partial<IntersectionObserverInit>}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyEvent${pascalName}': ${type} & DefineComponent<{hydrate?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyMedia${pascalName}': ${type} & DefineComponent<{hydrate?: string}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyIf${pascalName}': ${type} & DefineComponent<{hydrate?: unknown}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyIdle${pascalName}': ${type} & DelayedComponent<number>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyTime${pascalName}': ${type} & DelayedComponent<number>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyPromise${pascalName}': ${type} & DelayedComponent<Promise<unknown>>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyVisible${pascalName}': ${type} & DelayedComponent<IntersectionObserverInit>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyEvent${pascalName}': ${type} & DelayedComponent<keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyMedia${pascalName}': ${type} & DelayedComponent<string>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyIf${pascalName}': ${type} & DelayedComponent<unknown>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => ` 'LazyNever${pascalName}': ${type}`).join('\n')}
|
||||
}
|
||||
|
||||
@ -139,13 +140,13 @@ declare module 'vue' {
|
||||
|
||||
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyIdle${pascalName}: ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyTime${pascalName}: ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyPromise${pascalName}: ${type} & DefineComponent<{hydrate?: Promise<unknown>}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyVisible${pascalName}: ${type} & DefineComponent<{hydrate?: Partial<IntersectionObserverInit>}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyEvent${pascalName}: ${type} & DefineComponent<{hydrate?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyMedia${pascalName}: ${type} & DefineComponent<{hydrate?: string}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyIf${pascalName}: ${type} & DefineComponent<{hydrate?: unknown}>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyIdle${pascalName}: ${type} & DelayedComponent<number>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyTime${pascalName}: ${type} & DelayedComponent<number>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyPromise${pascalName}: ${type} & DelayedComponent<Promise<unknown>>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyVisible${pascalName}: ${type} & DelayedComponent<IntersectionObserverInit>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyEvent${pascalName}: ${type} & DelayedComponent<keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyMedia${pascalName}: ${type} & DelayedComponent<string>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyIf${pascalName}: ${type} & DelayedComponent<unknown>`).join('\n')}
|
||||
${componentTypes.map(([pascalName, type]) => `export const LazyNever${pascalName}: ${type}`).join('\n')}
|
||||
|
||||
export const componentNames: string[]
|
||||
|
@ -219,7 +219,6 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* Delayed component hydration
|
||||
*
|
||||
* This enables components to defer hydration until necessary, improving performance by not requesting all resources at once.
|
||||
* @type {boolean}
|
||||
*/
|
||||
delayedHydration: false,
|
||||
|
@ -2840,15 +2840,20 @@ describe('lazy import components', () => {
|
||||
expect(await page.locator('body').getByText('This should be visible at first with promise!').all()).toHaveLength(0)
|
||||
})
|
||||
it('keeps reactivity with models', async () => {
|
||||
const { page } = await renderPage('/lazy-import-components/model')
|
||||
const { page } = await renderPage('/lazy-import-components/model-event')
|
||||
expect(await page.locator('#count').textContent()).toBe('0')
|
||||
await page.locator('#count').click()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(await page.locator('#count').textContent()).toBe(`${i}`)
|
||||
await page.locator('#inc').click()
|
||||
}
|
||||
expect(await page.locator('#count').textContent()).toBe('10')
|
||||
})
|
||||
it('emits hydration events', async () => {
|
||||
const { page, consoleLogs } = await renderPage('/lazy-import-components/model-event')
|
||||
expect(consoleLogs.some(log => log.type === 'log' && log.text === 'Component hydrated')).toBeFalsy()
|
||||
await page.locator('#count').click()
|
||||
expect(consoleLogs.some(log => log.type === 'log' && log.text === 'Component hydrated')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('defineNuxtComponent watch duplicate', () => {
|
||||
|
15
test/fixtures/basic/pages/lazy-import-components/model-event.vue
vendored
Normal file
15
test/fixtures/basic/pages/lazy-import-components/model-event.vue
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<LazyEventDelayedModel
|
||||
v-model="model"
|
||||
@hydrated="log"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = ref(0)
|
||||
function log () {
|
||||
console.log('Component hydrated')
|
||||
}
|
||||
</script>
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<LazyEventDelayedModel v-model="model" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = ref(0)
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user