diff --git a/e2e/docs/.vuepress/theme/client/components/MarkdownContentHooks.vue b/e2e/docs/.vuepress/theme/client/components/MarkdownContentHooks.vue new file mode 100644 index 0000000000..1426d7d702 --- /dev/null +++ b/e2e/docs/.vuepress/theme/client/components/MarkdownContentHooks.vue @@ -0,0 +1,46 @@ + + + + + + mounted: {{ mounted }} {{ mountedCount }} + + + beforeUnmount: {{ beforeUnmount }} + + changedCount: {{ changedCount }} + + diff --git a/e2e/docs/.vuepress/theme/client/layouts/Layout.vue b/e2e/docs/.vuepress/theme/client/layouts/Layout.vue index 07519db48d..7888e994aa 100644 --- a/e2e/docs/.vuepress/theme/client/layouts/Layout.vue +++ b/e2e/docs/.vuepress/theme/client/layouts/Layout.vue @@ -1,5 +1,6 @@ @@ -18,6 +19,8 @@ const siteData = useSiteData() + + diff --git a/e2e/docs/content-hooks/content.md b/e2e/docs/content-hooks/content.md new file mode 100644 index 0000000000..a8badd3c11 --- /dev/null +++ b/e2e/docs/content-hooks/content.md @@ -0,0 +1,3 @@ +## title + +content diff --git a/e2e/tests/content-hooks.spec.ts b/e2e/tests/content-hooks.spec.ts new file mode 100644 index 0000000000..fdecd3928c --- /dev/null +++ b/e2e/tests/content-hooks.spec.ts @@ -0,0 +1,64 @@ +import { expect, test } from '@playwright/test' +import { IS_DEV } from '../utils/env' +import { readSourceMarkdown, writeSourceMarkdown } from '../utils/source' + +const updateMarkdownContent = async (): Promise => { + const content = await readSourceMarkdown('content-hooks/content.md') + await writeSourceMarkdown( + 'content-hooks/content.md', + `${content}\n\nUpdated content`, + ) +} + +const restoreMarkdownContent = async (): Promise => { + await writeSourceMarkdown('content-hooks/content.md', '## title\n\ncontent\n') +} + +test.afterAll(async () => { + await restoreMarkdownContent() +}) + +test('should call content mounted hook', async ({ page }) => { + const mountedLocator = page.locator( + '.markdown-content-hooks .markdown-content-mounted', + ) + await page.goto('content-hooks/content.html') + + await expect(mountedLocator).toHaveText( + 'mounted: /content-hooks/content.html 1', + ) + + // update content but mounted hook should not be called twice + await updateMarkdownContent() + await expect(mountedLocator).toHaveText( + 'mounted: /content-hooks/content.html 1', + ) +}) + +/** + * onContentChange hook should only called in development + */ +test('should call content change hook', async ({ page }) => { + const changeLocator = page.locator( + '.markdown-content-hooks .markdown-content-change', + ) + await page.goto('content-hooks/content.html') + + await updateMarkdownContent() + await expect(changeLocator).toHaveText(`changedCount: ${IS_DEV ? 1 : 0}`) // 1 + + await updateMarkdownContent() + await expect(changeLocator).toHaveText(`changedCount: ${IS_DEV ? 2 : 0}`) // 2 +}) + +test('should call content before unmount hook', async ({ page }) => { + const beforeUnmountLocator = page.locator( + '.markdown-content-hooks .markdown-content-before-unmount', + ) + await page.goto('content-hooks/content.html') + await page.locator('.e2e-theme-nav ul > li > a').nth(0).click() + + await expect(beforeUnmountLocator).toHaveText( + 'beforeUnmount: /content-hooks/content.html', + ) +}) diff --git a/packages/client/src/components/Content.ts b/packages/client/src/components/Content.ts index e58b46214d..59f7665976 100644 --- a/packages/client/src/components/Content.ts +++ b/packages/client/src/components/Content.ts @@ -1,5 +1,5 @@ import { computed, defineAsyncComponent, defineComponent, h } from 'vue' -import { usePageComponent } from '../composables/index.js' +import { runCallbacks, usePageComponent } from '../composables/index.js' import { resolveRoute } from '../router/index.js' /** @@ -26,6 +26,17 @@ export const Content = defineComponent({ ) }) - return () => h(ContentComponent.value) + return () => + h(ContentComponent.value, { + onVnodeMounted: () => { + runCallbacks('mounted') + }, + onVnodeUpdated: () => { + runCallbacks('change') + }, + onVnodeBeforeUnmount: () => { + runCallbacks('beforeUnmount') + }, + }) }, }) diff --git a/packages/client/src/composables/contentHooks.ts b/packages/client/src/composables/contentHooks.ts new file mode 100644 index 0000000000..dbc1fb3180 --- /dev/null +++ b/packages/client/src/composables/contentHooks.ts @@ -0,0 +1,31 @@ +import { onUnmounted } from 'vue' + +type LifeCycle = 'beforeUnmount' | 'change' | 'mounted' + +const hooks: Record unknown)[]> = { + mounted: [], + beforeUnmount: [], + change: [], +} + +const createHook = + (lifeCycle: LifeCycle) => + (fn: () => unknown): void => { + hooks[lifeCycle].push(fn) + onUnmounted(() => { + hooks[lifeCycle] = hooks[lifeCycle].filter((f) => f !== fn) + }) + } + +export const onContentChange = createHook('change') + +export const onContentMounted = createHook('mounted') + +export const onContentBeforeUnmount = createHook('beforeUnmount') + +/** + * Call all registered callbacks + */ +export const runCallbacks = (lifeCycle: LifeCycle): void => { + hooks[lifeCycle].forEach((fn) => fn()) +} diff --git a/packages/client/src/composables/index.ts b/packages/client/src/composables/index.ts index c834060742..aa34aa20bb 100644 --- a/packages/client/src/composables/index.ts +++ b/packages/client/src/composables/index.ts @@ -1,3 +1,4 @@ export * from './clientData.js' export * from './clientDataUtils.js' export * from './updateHead.js' +export * from './contentHooks.js'
+ mounted: {{ mounted }} {{ mountedCount }} +
+ beforeUnmount: {{ beforeUnmount }} +
changedCount: {{ changedCount }}