Skip to content

feat(client): add onContentUpdated composition API #1620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { onContentUpdated, useRoutePath } from 'vuepress/client'

const mounted = ref('')
const beforeUnmount = ref('')

const mountedCount = ref(0)
const updatedCount = ref(0)

const routePath = useRoutePath()

watch(routePath, () => {
updatedCount.value = 0
})

onContentUpdated((reason) => {
switch (reason) {
case 'mounted':
mounted.value = routePath.value
mountedCount.value++
break
case 'updated':
updatedCount.value++
break
case 'beforeUnmount':
beforeUnmount.value = routePath.value
break
default:
}
})
</script>

<template>
<div class="markdown-content-hooks">
<h3>markdown content hooks</h3>
<p class="markdown-content-mounted">
mounted: {{ mounted }} {{ mountedCount }}
</p>
<p class="markdown-content-beforeUnmount">
beforeUnmount: {{ beforeUnmount }}
</p>
<p class="markdown-content-updated">updatedCount: {{ updatedCount }}</p>
</div>
</template>
3 changes: 3 additions & 0 deletions e2e/docs/.vuepress/theme/client/layouts/Layout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Content, useSiteData } from 'vuepress/client'
import MarkdownContentHooks from '../components/MarkdownContentHooks.vue'

const siteData = useSiteData()
</script>
Expand All @@ -18,6 +19,8 @@ const siteData = useSiteData()
<main class="e2e-theme-content">
<Content />
</main>

<MarkdownContentHooks />
</div>
</template>

Expand Down
3 changes: 3 additions & 0 deletions e2e/docs/composables/on-content-updated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## title

content
69 changes: 69 additions & 0 deletions e2e/tests/composables/on-content-updated.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect, test } from '@playwright/test'

Check failure on line 1 in e2e/tests/composables/on-content-updated.spec.ts

View workflow job for this annotation

GitHub Actions / e2e (macos-latest, 18, vite)

[chromium] › tests/composables/on-content-updated.spec.ts:40:1 › should call content hook on beforeUnmount

1) [chromium] › tests/composables/on-content-updated.spec.ts:40:1 › should call content hook on beforeUnmount Test timeout of 30000ms exceeded.
import { BUNDLER, IS_DEV } from '../../utils/env'
import { readSourceMarkdown, writeSourceMarkdown } from '../../utils/source'

const updateMarkdownContent = async (): Promise<void> => {
const content = await readSourceMarkdown('composables/on-content-updated.md')
await writeSourceMarkdown(
'composables/on-content-updated.md',
`${content}\n\nUpdated content`,
)
}

const restoreMarkdownContent = async (): Promise<void> => {
await writeSourceMarkdown(
'composables/on-content-updated.md',
'## title\n\ncontent\n',
)
}

test.afterAll(async () => {
await restoreMarkdownContent()
})

test('should call content hook on mounted', async ({ page }) => {
await page.goto('composables/on-content-updated.html')
const mountedLocator = page.locator(
'.markdown-content-hooks .markdown-content-mounted',
)
await expect(mountedLocator).toHaveText(
'mounted: /composables/on-content-updated.html 1',
)

// update content but mounted hook should not be called twice
await updateMarkdownContent()
await expect(mountedLocator).toHaveText(
'mounted: /composables/on-content-updated.html 1',
)
})

test('should call content hook on beforeUnmount', async ({ page }) => {
await page.goto('composables/on-content-updated.html')

const beforeUnmountLocator = page.locator(
'.markdown-content-hooks .markdown-content-beforeUnmount',
)

await page.locator('.e2e-theme-nav ul > li > a').nth(0).click()

Check failure on line 47 in e2e/tests/composables/on-content-updated.spec.ts

View workflow job for this annotation

GitHub Actions / e2e (macos-latest, 18, vite)

[chromium] › tests/composables/on-content-updated.spec.ts:40:1 › should call content hook on beforeUnmount

1) [chromium] › tests/composables/on-content-updated.spec.ts:40:1 › should call content hook on beforeUnmount Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for locator('.e2e-theme-nav ul > li > a').first() 45 | ) 46 | > 47 | await page.locator('.e2e-theme-nav ul > li > a').nth(0).click() | ^ 48 | 49 | await expect(beforeUnmountLocator).toHaveText('beforeUnmount: /') 50 | }) at /Users/runner/work/core/core/e2e/tests/composables/on-content-updated.spec.ts:47:59

await expect(beforeUnmountLocator).toHaveText('beforeUnmount: /')
})

/**
* Updated hooks are only supported for use in development environments.
* In CI environments, under both Linux and Windows, using Vite fails to correctly trigger hooks.
*/
if (IS_DEV && BUNDLER !== 'vite') {
test('should call content hook on updated', async ({ page }) => {
await page.goto('composables/on-content-updated.html')
const updatedLocator = page.locator(
'.markdown-content-hooks .markdown-content-updated',
)

await updateMarkdownContent()
await expect(updatedLocator).toHaveText(`updatedCount: 1`)

await updateMarkdownContent()
await expect(updatedLocator).toHaveText(`updatedCount: 2`)
})
}
37 changes: 34 additions & 3 deletions packages/client/src/components/Content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { computed, defineAsyncComponent, defineComponent, h } from 'vue'
import { usePageComponent } from '../composables/index.js'
import { computed, defineAsyncComponent, defineComponent, h, watch } from 'vue'
import { usePageComponent, usePageFrontmatter } from '../composables/index.js'
import { contentUpdatedCallbacks } from '../internal/contentUpdatedCallbacks'
import { resolveRoute } from '../router/index.js'
import type { ContentUpdatedReason } from '../types/index.js'

/**
* Execute all callbacks registered via `onContentUpdated`.
*
* @internal
*/
const runContentUpdatedCallbacks = (reason: ContentUpdatedReason): void => {
contentUpdatedCallbacks.value.forEach((fn) => fn(reason))
}

/**
* Markdown rendered content
Expand All @@ -26,6 +37,26 @@ export const Content = defineComponent({
)
})

return () => h(ContentComponent.value)
const frontmatter = usePageFrontmatter()
watch(
frontmatter,
() => {
runContentUpdatedCallbacks('updated')
},
{ deep: true, flush: 'post' },
)

return () =>
h(ContentComponent.value, {
onVnodeMounted: () => {
runContentUpdatedCallbacks('mounted')
},
onVnodeUpdated: () => {
runContentUpdatedCallbacks('updated')
},
onVnodeBeforeUnmount: () => {
runContentUpdatedCallbacks('beforeUnmount')
},
})
},
})
1 change: 1 addition & 0 deletions packages/client/src/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './clientData.js'
export * from './clientDataUtils.js'
export * from './onContentUpdated.js'
export * from './updateHead.js'
16 changes: 16 additions & 0 deletions packages/client/src/composables/onContentUpdated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { onUnmounted } from 'vue'
import { contentUpdatedCallbacks } from '../internal/contentUpdatedCallbacks'
import type { ContentUpdatedCallback } from '../types/index.js'

/**
* Register callback that is called every time the markdown content is updated
* in the DOM.
*/
export const onContentUpdated = (fn: ContentUpdatedCallback): void => {
contentUpdatedCallbacks.value.push(fn)
onUnmounted(() => {
contentUpdatedCallbacks.value = contentUpdatedCallbacks.value.filter(
(f) => f !== fn,
)
})
}
9 changes: 9 additions & 0 deletions packages/client/src/internal/contentUpdatedCallbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Ref } from 'vue'
import { shallowRef } from 'vue'
import type { ContentUpdatedCallback } from '../types/index.js'

/**
* Global content updated callbacks ref
*/
export const contentUpdatedCallbacks: Ref<ContentUpdatedCallback[]> =
shallowRef([])
1 change: 1 addition & 0 deletions packages/client/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type * from './clientConfig.js'
export type * from './clientData.js'
export type * from './onContentUpdated.js'
export type * from './createVueAppFunction.js'
export type * from './routes.js'
3 changes: 3 additions & 0 deletions packages/client/src/types/onContentUpdated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ContentUpdatedReason = 'beforeUnmount' | 'mounted' | 'updated'

export type ContentUpdatedCallback = (reason: ContentUpdatedReason) => unknown
Loading