Skip to content

Commit

Permalink
feat(client): add onContentUpdated composition API (#1620)
Browse files Browse the repository at this point in the history
Co-authored-by: Mister-Hope <[email protected]>
Co-authored-by: Xinyu Liu <[email protected]>
  • Loading branch information
3 people authored Feb 23, 2025
1 parent c971b79 commit cd7252f
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 3 deletions.
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'
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()

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

0 comments on commit cd7252f

Please sign in to comment.