Skip to content
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

feat: support for teleports to body #1642

Merged
merged 10 commits into from
Jan 14, 2023
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
78 changes: 78 additions & 0 deletions docs/components/ModalDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { ref } from 'vue'
const showModal = ref(false)
</script>

<template>
<button class="modal-button" @click="showModal = true">Show Modal</button>

<Teleport to="body">
<Transition name="modal">
<div v-show="showModal" class="modal-mask">
<div class="modal-container">
<p>Hello from the modal!</p>
<div class="model-footer">
<button class="modal-button" @click="showModal = false">
Close
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>

<style scoped>
.modal-mask {
position: fixed;
z-index: 200;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
}

.modal-container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: var(--vp-c-bg);
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}

.model-footer {
margin-top: 8px;
text-align: right;
}

.modal-button {
padding: 4px 8px;
border-radius: 4px;
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
}

.modal-button:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}

.modal-enter-from,
.modal-leave-to {
opacity: 0;
}

.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
transform: scale(1.1);
}
</style>
55 changes: 39 additions & 16 deletions docs/config/app-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,47 @@ VitePress build hooks allow you to add new functionality and behaviors to your w
- Sitemap
- Search Indexing
- PWA
- Teleports

### buildEnd

- Type: `(siteConfig: SiteConfig) => Awaitable<void>`

`buildEnd` is a build CLI hook, it will run after build (SSG) finish but before VitePress CLI process exits.

```ts
export default {
async buildEnd(siteConfig) {
// ...
}
}
```

### postRender

- Type: `(context: SSGContext) => Awaitable<SSGContext | void>`

`postRender` is a build hook, called when SSG rendering is done. It will allow you to handle the teleports content during SSG.

```ts
export default {
async postRender(context) {
// ...
}
}
```

```ts
interface SSGContext {
content: string
teleports?: Record<string, string>
[key: string]: any
}
```

### transformHead

- Type: `(ctx: TransformContext) => Awaitable<HeadConfig[]>`
- Type: `(context: TransformContext) => Awaitable<HeadConfig[]>`

`transformHead` is a build hook to transform the head before generating each page. It will allow you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones.

Expand All @@ -311,7 +348,7 @@ Don't mutate anything inside the `ctx`.

```ts
export default {
async transformHead(ctx) {
async transformHead(context) {
// ...
}
}
Expand Down Expand Up @@ -367,17 +404,3 @@ export default {
}
}
```

### buildEnd

- Type: `(siteConfig: SiteConfig) => Awaitable<void>`

`buildEnd` is a build CLI hook, it will run after build (SSG) finish but before VitePress CLI process exits.

```ts
export default {
async buildEnd(siteConfig) {
// ...
}
}
```
24 changes: 24 additions & 0 deletions docs/guide/using-vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,27 @@ export default {
**Also see:**

- [Vue.js > Dynamic Components](https://vuejs.org/guide/essentials/component-basics.html#dynamic-components)

## Using Teleports

Vitepress currently has SSG support for teleports to body only. For other targets, you can wrap them inside the built-in `<ClientOnly>` component or inject the teleport markup into the correct location in your final page HTML through [`postRender` hook](../config/app-configs#postrender).

<ModalDemo />

::: details
<<< @/components/ModalDemo.vue
:::

```md
<ClientOnly>
<Teleport to="#modal">
<div>
// ...
</div>
</Teleport>
</ClientOnly>
```

<script setup>
import ModalDemo from '../components/ModalDemo.vue'
</script>
5 changes: 4 additions & 1 deletion src/client/app/ssr.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// entry for SSR
import { createApp } from './index.js'
import { renderToString } from 'vue/server-renderer'
import type { SSGContext } from '../shared.js'

export async function render(path: string) {
const { app, router } = await createApp()
await router.go(path)
return renderToString(app)
const ctx: SSGContext = { content: '' }
ctx.content = await renderToString(app, ctx)
return ctx
}
24 changes: 13 additions & 11 deletions src/node/build/render.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import escape from 'escape-html'
import fs from 'fs-extra'
import path from 'path'
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup'
import { pathToFileURL } from 'url'
import escape from 'escape-html'
import { normalizePath, transformWithEsbuild } from 'vite'
import type { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { resolveSiteDataByRoute, type SiteConfig } from '../config'
import type { SSGContext } from '../shared'
import {
type HeadConfig,
type PageData,
createTitle,
notFoundPageData,
mergeHead,
EXTERNAL_URL_RE,
sanitizeFileName
mergeHead,
notFoundPageData,
sanitizeFileName,
type HeadConfig,
type PageData
} from '../shared'
import { slash } from '../utils/slash'
import { type SiteConfig, resolveSiteDataByRoute } from '../config'

export async function renderPage(
render: (path: string) => Promise<string>,
render: (path: string) => Promise<SSGContext>,
config: SiteConfig,
page: string, // foo.md
result: RollupOutput | null,
Expand All @@ -30,7 +31,8 @@ export async function renderPage(
const siteData = resolveSiteDataByRoute(config.site, routePath)

// render page
const content = await render(routePath)
const context = await render(routePath)
const { content, teleports } = (await config.postRender?.(context)) ?? context

const pageName = sanitizeFileName(page.replace(/\//g, '_'))
// server build doesn't need hash
Expand Down Expand Up @@ -155,7 +157,7 @@ export async function renderPage(
${prefetchLinkString}
${await renderHead(head)}
</head>
<body>
<body>${teleports?.body || ''}
<div id="app">${content}</div>
${
config.mpa
Expand Down
38 changes: 23 additions & 15 deletions src/node/config.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import path from 'path'
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import _debug from 'debug'
import fg from 'fast-glob'
import fs from 'fs-extra'
import path from 'path'
import c from 'picocolors'
import fg from 'fast-glob'
import {
normalizePath,
type UserConfig as ViteConfig,
loadConfigFromFile,
mergeConfig as mergeViteConfig,
loadConfigFromFile
normalizePath,
type UserConfig as ViteConfig
} from 'vite'
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import type { SSGContext } from '../../types/shared'
import { DEFAULT_THEME_PATH } from './alias'
import type { MarkdownOptions } from './markdown/markdown'
import {
type SiteData,
type HeadConfig,
type LocaleConfig,
type DefaultTheme,
APPEARANCE_KEY,
createLangDictionary,
type Awaitable,
type CleanUrlsMode,
type DefaultTheme,
type HeadConfig,
type LocaleConfig,
type PageData,
type Awaitable
type SiteData
} from './shared'
import { DEFAULT_THEME_PATH } from './alias'
import type { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'

export { resolveSiteDataByRoute } from './shared'

Expand Down Expand Up @@ -104,12 +105,17 @@ export interface UserConfig<ThemeConfig = any> {
*/
buildEnd?: (siteConfig: SiteConfig) => Awaitable<void>

/**
* Render end hook: called when SSR rendering is done.
*/
postRender?: (context: SSGContext) => Awaitable<SSGContext | void>

/**
* Head transform hook: runs before writing HTML to dist.
*
* This build hook will allow you to modify the head adding new entries that cannot be statically added.
*/
transformHead?: (ctx: TransformContext) => Awaitable<HeadConfig[]>
transformHead?: (context: TransformContext) => Awaitable<HeadConfig[]>

/**
* HTML transform hook: runs before writing HTML to dist.
Expand Down Expand Up @@ -154,6 +160,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'ignoreDeadLinks'
| 'cleanUrls'
| 'useWebFonts'
| 'postRender'
| 'buildEnd'
| 'transformHead'
| 'transformHtml'
Expand Down Expand Up @@ -250,6 +257,7 @@ export async function resolveConfig(
useWebFonts:
userConfig.useWebFonts ??
typeof process.versions.webcontainer === 'string',
postRender: userConfig.postRender,
buildEnd: userConfig.buildEnd,
transformHead: userConfig.transformHead,
transformHtml: userConfig.transformHtml,
Expand Down
19 changes: 10 additions & 9 deletions src/shared/shared.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import type {
SiteData,
PageData,
HeadConfig,
LocaleConfig,
HeadConfig
PageData,
SiteData
} from '../../types/shared.js'

export type {
SiteData,
PageData,
Awaitable,
CleanUrlsMode,
DefaultTheme,
HeadConfig,
LocaleConfig,
Header,
DefaultTheme,
LocaleConfig,
PageData,
PageDataPayload,
CleanUrlsMode,
Awaitable
SiteData,
SSGContext
} from '../../types/shared.js'

export const EXTERNAL_URL_RE = /^[a-z]+:/i
Expand Down
5 changes: 5 additions & 0 deletions types/shared.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// types shared between server and client
import type { SSRContext } from 'vue/server-renderer'
export type { DefaultTheme } from './default-theme.js'

export type Awaitable<T> = T | PromiseLike<T>
Expand Down Expand Up @@ -107,3 +108,7 @@ export interface PageDataPayload {
path: string
pageData: PageData
}

export interface SSGContext extends SSRContext {
content: string
}