mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-23 22:40:34 +03:00
feat: add support for computed metadata
feat: show active metadata and add some styling chore: code cleanup
This commit is contained in:
@@ -7,16 +7,54 @@ html, body {
|
|||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
|
||||||
line-height: 1.5em;
|
|
||||||
padding-left: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #7f8c8d;
|
color: #2c3e50;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #4fc08d;
|
color: #4fc08d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.menu {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu li {
|
||||||
|
margin: 0;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
border: 1px solid #2c3e50;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata h4 {
|
||||||
|
padding: 15px;
|
||||||
|
border-top-left-radius: 1rem;
|
||||||
|
border-top-right-radius: 1rem;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata p {
|
||||||
|
font-size: 75%;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 400px;
|
||||||
|
max-height: 50%;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,16 +177,21 @@ export default {
|
|||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<h1>vue-router</h1>
|
<h1>vue-router</h1>
|
||||||
<router-link to="/">Home</router-link>
|
<ul class="menu">
|
||||||
<router-link to="/about">About</router-link>
|
<li><router-link to="/">Home</router-link></li>
|
||||||
|
<li><router-link to="/about">About</router-link></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }" class="page">
|
||||||
<transition name="page" mode="out-in">
|
<transition name="page" mode="out-in">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<p>Inspect Element to see the meta info</p>
|
<div class="metadata">
|
||||||
|
<h4>Active Metainfo:</h4>
|
||||||
|
<p>{{ JSON.stringify(metadata, null, 2)}}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
import { defineComponent, reactive, toRefs } from 'vue'
|
import { defineComponent, reactive, computed, toRefs, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useMeta } from 'vue-meta'
|
import { useMeta } from 'vue-meta'
|
||||||
|
|
||||||
const metaUpdated = 'no' // TODO: afterNavigation hook?
|
let metaUpdated = 'no'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ChildComponent',
|
name: 'ChildComponent',
|
||||||
props: {
|
setup () {
|
||||||
page: {
|
const route = useRoute()
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
date: null,
|
date: null,
|
||||||
metaUpdated
|
metaUpdated
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = props.page[0].toUpperCase() + props.page.slice(1)
|
const metaConfig = computed(() => ({
|
||||||
console.log('ChildComponent Setup')
|
|
||||||
|
|
||||||
useMeta({
|
|
||||||
charset: 'utf16',
|
charset: 'utf16',
|
||||||
title,
|
title: route.name[0].toUpperCase() + route.name.slice(1),
|
||||||
description: 'Description ' + props.page,
|
description: 'Description ' + route.name,
|
||||||
og: {
|
og: {
|
||||||
title: 'Og Title ' + props.page
|
title: 'Og Title ' + route.name
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { onRemoved } = useMeta(metaConfig)
|
||||||
|
|
||||||
|
const pageName = computed(() => route.name)
|
||||||
|
|
||||||
|
onUnmounted(() => (metaUpdated = 'yes'))
|
||||||
|
|
||||||
|
onRemoved(() => {
|
||||||
|
console.log('Meta was removed', pageName.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
return toRefs(state)
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
pageName
|
||||||
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `<div>
|
||||||
<div>
|
<h3>You're looking at the <strong>{{ pageName }}</strong> page</h3>
|
||||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
|
||||||
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
|
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
|
||||||
</div>
|
</div>`
|
||||||
`
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ import { createMetaManager, defaultConfig, resolveOption, useMeta } from 'vue-me
|
|||||||
import App from './App'
|
import App from './App'
|
||||||
import ChildComponent from './Child'
|
import ChildComponent from './Child'
|
||||||
|
|
||||||
function createView (page) {
|
function createComponent () {
|
||||||
return {
|
return {
|
||||||
name: `section-${page}`,
|
render: () => h(ChildComponent)
|
||||||
render () {
|
|
||||||
return h(ChildComponent, { page })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,14 +38,13 @@ useMeta(
|
|||||||
const createRouter = (base, isSSR) => createVueRouter({
|
const createRouter = (base, isSSR) => createVueRouter({
|
||||||
history: isSSR ? createMemoryHistory(base) : createWebHistory(base),
|
history: isSSR ? createMemoryHistory(base) : createWebHistory(base),
|
||||||
routes: [
|
routes: [
|
||||||
{ name: 'home', path: '/', component: createView('home') },
|
{ name: 'home', path: '/', component: createComponent() },
|
||||||
{ name: 'about', path: '/about', component: createView('about') }
|
{ name: 'about', path: '/about', component: createComponent() }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export {
|
export {
|
||||||
App,
|
App,
|
||||||
metaManager,
|
metaManager,
|
||||||
createRouter,
|
createRouter
|
||||||
createView
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-21
@@ -53,36 +53,37 @@
|
|||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.16",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
|
"@babel/plugin-transform-modules-commonjs": "^7.12.13",
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.16",
|
||||||
"@nuxtjs/eslint-config-typescript": "^5.0.0",
|
"@nuxtjs/eslint-config-typescript": "^5.0.0",
|
||||||
"@rollup/plugin-alias": "^3.1.1",
|
"@rollup/plugin-alias": "^3.1.2",
|
||||||
"@rollup/plugin-commonjs": "^17.0.0",
|
"@rollup/plugin-commonjs": "^17.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^11.1.0",
|
"@rollup/plugin-node-resolve": "^11.2.0",
|
||||||
"@rollup/plugin-replace": "^2.3.4",
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
"@types/webpack": "^4.41.26",
|
"@types/webpack": "^4.41.26",
|
||||||
"@types/webpack-env": "^1.16.0",
|
"@types/webpack-env": "^1.16.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||||
"@typescript-eslint/parser": "^4.14.0",
|
"@typescript-eslint/parser": "^4.15.0",
|
||||||
"@vue/compiler-sfc": "^3.0.5",
|
"@vue/compiler-sfc": "^3.0.5",
|
||||||
"@vue/server-renderer": "^3.0.5",
|
"@vue/server-renderer": "^3.0.5",
|
||||||
"@vue/server-test-utils": "^1.1.2",
|
"@vue/server-test-utils": "^1.1.3",
|
||||||
"@vue/test-utils": "^1.1.2",
|
"@vue/test-utils": "^1.1.3",
|
||||||
"@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5",
|
"@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||||
"babel-plugin-global-define": "^1.0.3",
|
"babel-plugin-global-define": "^1.0.3",
|
||||||
"babel-plugin-module-resolver": "^4.1.0",
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
|
"babel-preset-vue": "^2.0.2",
|
||||||
"browserstack-local": "^1.4.8",
|
"browserstack-local": "^1.4.8",
|
||||||
"chromedriver": "^88.0.0",
|
"chromedriver": "^88.0.0",
|
||||||
"codecov": "^3.8.1",
|
"codecov": "^3.8.1",
|
||||||
"consola": "^2.15.0",
|
"consola": "^2.15.3",
|
||||||
"eslint": "^7.18.0",
|
"eslint": "^7.20.0",
|
||||||
"express-urlrewrite": "^1.4.0",
|
"express-urlrewrite": "^1.4.0",
|
||||||
"geckodriver": "^1.21.1",
|
"geckodriver": "^1.22.1",
|
||||||
"html-webpack-plugin": "^4.5.1",
|
"html-webpack-plugin": "^5.1.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-environment-jsdom": "^26.6.2",
|
"jest-environment-jsdom": "^26.6.2",
|
||||||
"jest-environment-jsdom-global": "^2.0.4",
|
"jest-environment-jsdom-global": "^2.0.4",
|
||||||
@@ -90,25 +91,26 @@
|
|||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"node-env-file": "^0.1.8",
|
"node-env-file": "^0.1.8",
|
||||||
"puppeteer-core": "^5.5.0",
|
"puppeteer-core": "^7.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.38.0",
|
"rollup": "^2.39.0",
|
||||||
"rollup-plugin-dts": "^2.0.1",
|
"rollup-plugin-dts": "^2.0.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.29.0",
|
"rollup-plugin-typescript2": "^0.29.0",
|
||||||
"selenium-webdriver": "^4.0.0-alpha.8",
|
"selenium-webdriver": "^4.0.0-alpha.8",
|
||||||
"standard-version": "^9.1.0",
|
"standard-version": "^9.1.0",
|
||||||
"tib": "^0.7.5",
|
"tib": "^0.7.5",
|
||||||
"ts-jest": "^26.4.4",
|
"ts-jest": "^26.5.1",
|
||||||
"ts-loader": "^8.0.14",
|
"ts-loader": "^8.0.17",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.5",
|
||||||
|
"vite": "^2.0.4",
|
||||||
"vue": "^3.0.5",
|
"vue": "^3.0.5",
|
||||||
"vue-jest": "^3.0.7",
|
"vue-jest": "^3.0.7",
|
||||||
"vue-loader": "^16.0.0",
|
"vue-loader": "^16.0.0",
|
||||||
"vue-router": "next",
|
"vue-router": "next",
|
||||||
"webpack": "^5.17.0",
|
"webpack": "^5.21.2",
|
||||||
"webpack-bundle-analyzer": "^4.4.0",
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
"webpack-cli": "^4.4.0",
|
"webpack-cli": "^4.5.0",
|
||||||
"webpack-dev-server": "^3.11.2",
|
"webpack-dev-server": "^3.11.2",
|
||||||
"webpackbar": "^4.0.0"
|
"webpackbar": "^4.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+171
-102
@@ -1,7 +1,7 @@
|
|||||||
import { h, reactive, onUnmounted, Teleport, Comment, getCurrentInstance } from 'vue'
|
import { h, reactive, onUnmounted, App, Teleport, Comment, getCurrentInstance, ComponentInternalInstance, Slots } from 'vue'
|
||||||
import type { VNode } from 'vue'
|
import type { VNode, ComponentPublicInstance } from 'vue'
|
||||||
import { isArray, isFunction } from '@vue/shared'
|
import { isArray, isFunction } from '@vue/shared'
|
||||||
import { createMergedObject } from './object-merge'
|
import { createMergedObject, MergedObjectBuilder } from './object-merge'
|
||||||
import { renderMeta } from './render'
|
import { renderMeta } from './render'
|
||||||
import { metaActiveKey } from './symbols'
|
import { metaActiveKey } from './symbols'
|
||||||
import { Metainfo } from './Metainfo'
|
import { Metainfo } from './Metainfo'
|
||||||
@@ -9,10 +9,14 @@ import type { ResolveMethod } from './object-merge'
|
|||||||
import type {
|
import type {
|
||||||
MetaActive,
|
MetaActive,
|
||||||
MetaConfig,
|
MetaConfig,
|
||||||
MetaManager,
|
MetaGuards,
|
||||||
|
MetaGuardRemoved,
|
||||||
MetaResolver,
|
MetaResolver,
|
||||||
MetaResolveContext,
|
MetaResolveContext,
|
||||||
MetaTeleports
|
MetaTeleports,
|
||||||
|
MetaSource,
|
||||||
|
MetaResolverSetup,
|
||||||
|
MetaProxy
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export const ssrAttribute = 'data-vm-ssr'
|
export const ssrAttribute = 'data-vm-ssr'
|
||||||
@@ -46,127 +50,192 @@ export function addVnode (teleports: MetaTeleports, to: string, vnodes: VNode |
|
|||||||
teleports[to].push(...nodes)
|
teleports[to].push(...nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMetaManager (config: MetaConfig, resolver: MetaResolver | ResolveMethod): MetaManager {
|
// eslint-disable-next-line no-use-before-define
|
||||||
let cleanedUpSsr = false
|
export type createMetaManagerMethod = (config: MetaConfig, resolver: MetaResolver | ResolveMethod) => MetaManager
|
||||||
|
|
||||||
const resolve: ResolveMethod = (options, contexts, active, key, pathSegments) => {
|
export const createMetaManager: createMetaManagerMethod = (config, resolver) => MetaManager.create(config, resolver)
|
||||||
if (isFunction(resolver)) {
|
|
||||||
return resolver(options, contexts, active, key, pathSegments)
|
export class MetaManager {
|
||||||
|
config: MetaConfig
|
||||||
|
target: MergedObjectBuilder
|
||||||
|
resolver?: MetaResolverSetup
|
||||||
|
|
||||||
|
ssrCleanedUp: boolean = false
|
||||||
|
|
||||||
|
constructor (config: MetaConfig, target: MergedObjectBuilder, resolver: MetaResolver | ResolveMethod) {
|
||||||
|
this.config = config
|
||||||
|
this.target = target
|
||||||
|
|
||||||
|
if (resolver && 'setup' in resolver && isFunction(resolver.setup)) {
|
||||||
|
this.resolver = resolver as unknown as MetaResolverSetup
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolver.resolve(options, contexts, active, key, pathSegments)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addSource, delSource } = createMergedObject(resolve, active)
|
static create: createMetaManagerMethod = (config, resolver) => {
|
||||||
|
const resolve: ResolveMethod = (options, contexts, active, key, pathSegments) => {
|
||||||
// TODO: validate resolver
|
if (isFunction(resolver)) {
|
||||||
const manager: MetaManager = {
|
return resolver(options, contexts, active, key, pathSegments)
|
||||||
config,
|
|
||||||
|
|
||||||
install (app) {
|
|
||||||
app.component('Metainfo', Metainfo)
|
|
||||||
|
|
||||||
app.config.globalProperties.$metaManager = manager
|
|
||||||
app.provide(metaActiveKey, active)
|
|
||||||
},
|
|
||||||
|
|
||||||
addMeta (metaObj, vm) {
|
|
||||||
if (!vm) {
|
|
||||||
vm = getCurrentInstance() || undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveContext: MetaResolveContext = { vm }
|
return resolver.resolve(options, contexts, active, key, pathSegments)
|
||||||
if (resolver && 'setup' in resolver && isFunction(resolver.setup)) {
|
}
|
||||||
resolver.setup(resolveContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: optimize initial compute
|
const mergedObject = createMergedObject(resolve, active)
|
||||||
const meta = addSource(metaObj, resolveContext, true)
|
|
||||||
|
|
||||||
const unmount = () => delSource(meta)
|
// TODO: validate resolver
|
||||||
if (vm) {
|
const manager = new MetaManager(config, mergedObject, resolver)
|
||||||
onUnmounted(unmount)
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
install (app: App): void {
|
||||||
meta,
|
app.component('Metainfo', Metainfo)
|
||||||
unmount
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render ({ slots } = {}) {
|
app.config.globalProperties.$metaManager = this
|
||||||
// cleanup ssr tags if not yet done
|
app.provide(metaActiveKey, active)
|
||||||
if (__BROWSER__ && !cleanedUpSsr) {
|
}
|
||||||
cleanedUpSsr = true
|
|
||||||
|
|
||||||
// Listen for DOM loaded because tags in the body couldnt
|
addMeta (metadata: MetaSource, vm?: ComponentInternalInstance): MetaProxy {
|
||||||
// have loaded yet once the manager does it first render
|
if (!vm) {
|
||||||
// (preferable there should only be one meta render on hydration)
|
vm = getCurrentInstance() || undefined
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
}
|
||||||
const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`)
|
|
||||||
|
|
||||||
if (ssrTags && ssrTags.length) {
|
const metaGuards: MetaGuards = ({
|
||||||
Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el))
|
removed: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolveContext: MetaResolveContext = { vm }
|
||||||
|
if (this.resolver) {
|
||||||
|
this.resolver.setup(resolveContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: optimize initial compute (once)
|
||||||
|
const meta = this.target.addSource(metadata, resolveContext, true)
|
||||||
|
|
||||||
|
const onRemoved = (removeGuard: MetaGuardRemoved) => metaGuards.removed.push(removeGuard)
|
||||||
|
|
||||||
|
const unmount = (ignoreGuards?: boolean) => this.unmount(!!ignoreGuards, meta, metaGuards, vm)
|
||||||
|
|
||||||
|
if (vm) {
|
||||||
|
onUnmounted(unmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta,
|
||||||
|
onRemoved,
|
||||||
|
unmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unmount (ignoreGuards: boolean, meta: any, metaGuards: MetaGuards, vm?: ComponentInternalInstance) {
|
||||||
|
if (vm) {
|
||||||
|
const { $el } = vm.proxy as unknown as ComponentPublicInstance
|
||||||
|
|
||||||
|
// Wait for element to be removed from DOM
|
||||||
|
if ($el && $el.offsetParent) {
|
||||||
|
let observer: MutationObserver | undefined = new MutationObserver((records) => {
|
||||||
|
for (const { removedNodes } of records) {
|
||||||
|
if (!removedNodes) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
removedNodes.forEach((el) => {
|
||||||
|
if (el === $el && observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
observer = undefined
|
||||||
|
this.reallyUnmount(ignoreGuards, meta, metaGuards)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
observer.observe($el.parentNode, { childList: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reallyUnmount(ignoreGuards, meta, metaGuards)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reallyUnmount (ignoreGuards: boolean, meta: any, metaGuards: MetaGuards): Promise<void> {
|
||||||
|
this.target.delSource(meta)
|
||||||
|
|
||||||
|
if (!ignoreGuards && metaGuards) {
|
||||||
|
await Promise.all(metaGuards.removed.map(removeGuard => removeGuard()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render ({ slots }: { slots?: Slots } = {}): VNode[] {
|
||||||
|
// TODO: clean this method
|
||||||
|
|
||||||
|
// cleanup ssr tags if not yet done
|
||||||
|
if (__BROWSER__ && !this.ssrCleanedUp) {
|
||||||
|
this.ssrCleanedUp = true
|
||||||
|
|
||||||
|
// Listen for DOM loaded because tags in the body couldnt
|
||||||
|
// have loaded yet once the manager does it first render
|
||||||
|
// (preferable there should only be one meta render on hydration)
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`)
|
||||||
|
|
||||||
|
if (ssrTags && ssrTags.length) {
|
||||||
|
Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const teleports: MetaTeleports = {}
|
||||||
|
|
||||||
|
for (const key in active) {
|
||||||
|
const config = this.config[key] || {}
|
||||||
|
|
||||||
|
let renderedNodes = renderMeta(
|
||||||
|
{ metainfo: active, slots },
|
||||||
|
key,
|
||||||
|
active[key],
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!renderedNodes) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const teleports: MetaTeleports = {}
|
if (!isArray(renderedNodes)) {
|
||||||
|
renderedNodes = [renderedNodes]
|
||||||
|
}
|
||||||
|
|
||||||
for (const key in active) {
|
let defaultTo = key !== 'base' && active[key].to
|
||||||
const config = this.config[key] || {}
|
|
||||||
|
|
||||||
let renderedNodes = renderMeta(
|
if (!defaultTo && 'to' in config) {
|
||||||
{ metainfo: active, slots },
|
defaultTo = config.to
|
||||||
key,
|
}
|
||||||
active[key],
|
|
||||||
config
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!renderedNodes) {
|
if (!defaultTo && 'attributesFor' in config) {
|
||||||
|
defaultTo = key
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { to, vnode } of renderedNodes) {
|
||||||
|
addVnode(teleports, to || defaultTo || 'head', vnode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slots) {
|
||||||
|
for (const slotName in slots) {
|
||||||
|
const tagName = slotName === 'default' ? 'head' : slotName
|
||||||
|
|
||||||
|
// Only teleport the contents of head/body slots
|
||||||
|
if (tagName !== 'head' && tagName !== 'body') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isArray(renderedNodes)) {
|
const slot = slots[slotName]
|
||||||
renderedNodes = [renderedNodes]
|
if (isFunction(slot)) {
|
||||||
}
|
addVnode(teleports, tagName, slot({ metainfo: active }))
|
||||||
|
|
||||||
let defaultTo = key !== 'base' && active[key].to
|
|
||||||
|
|
||||||
if (!defaultTo && 'to' in config) {
|
|
||||||
defaultTo = config.to
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!defaultTo && 'attributesFor' in config) {
|
|
||||||
defaultTo = key
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { to, vnode } of renderedNodes) {
|
|
||||||
addVnode(teleports, to || defaultTo || 'head', vnode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slots) {
|
|
||||||
for (const slotName in slots) {
|
|
||||||
const tagName = slotName === 'default' ? 'head' : slotName
|
|
||||||
|
|
||||||
// Only teleport the contents of head/body slots
|
|
||||||
if (tagName !== 'head' && tagName !== 'body') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const slot = slots[slotName]
|
|
||||||
if (isFunction(slot)) {
|
|
||||||
addVnode(teleports, tagName, slot({ metainfo: active }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(teleports).map((to) => {
|
|
||||||
return h(Teleport, { to }, teleports[to])
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return manager
|
return Object.keys(teleports).map((to) => {
|
||||||
|
return h(Teleport, { to }, teleports[to])
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { isArray, isString } from '@vue/shared'
|
import { isArray, isFunction, isString } from '@vue/shared'
|
||||||
import { getTagConfigItem } from './config'
|
import { getTagConfigItem } from './config'
|
||||||
import type {
|
import type {
|
||||||
MetaConfigSectionAttribute,
|
MetaConfigSectionAttribute,
|
||||||
@@ -279,7 +279,7 @@ export function getSlotContent (
|
|||||||
groupConfig?: MetaGroupConfig
|
groupConfig?: MetaGroupConfig
|
||||||
): string {
|
): string {
|
||||||
const slot = slots && slots[slotName]
|
const slot = slots && slots[slotName]
|
||||||
if (!slot) {
|
if (!slot || !isFunction(slot)) {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,5 +10,5 @@ export const PolySymbol = (name: string) =>
|
|||||||
: (__DEV__ ? '[vue-meta]: ' : '_vm_') + name
|
: (__DEV__ ? '[vue-meta]: ' : '_vm_') + name
|
||||||
|
|
||||||
export const metaActiveKey = /*#__PURE__*/ PolySymbol(
|
export const metaActiveKey = /*#__PURE__*/ PolySymbol(
|
||||||
__DEV__ ? 'active_meta' : 'am'
|
__DEV__ ? 'meta_active' : 'ma'
|
||||||
) as InjectionKey<MetaActive>
|
) as InjectionKey<MetaActive>
|
||||||
|
|||||||
+19
-14
@@ -1,9 +1,11 @@
|
|||||||
import type { App, VNode, Slots, ComponentInternalInstance } from 'vue'
|
import type { VNode, Slots, ComponentInternalInstance } from 'vue'
|
||||||
import type { MergedObject, ResolveContext, ResolveMethod } from '../object-merge'
|
import type { MergedObject, ResolveContext, ResolveMethod } from '../object-merge'
|
||||||
import type { MetaConfig } from './config'
|
import type { MetaManager } from '../manager'
|
||||||
|
|
||||||
export * from './config'
|
export * from './config'
|
||||||
|
|
||||||
|
export type Modify<T, R> = Omit<T, keyof R> & R;
|
||||||
|
|
||||||
export type TODO = any
|
export type TODO = any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,12 +20,15 @@ export type MetaSource = {
|
|||||||
[key: string]: TODO
|
[key: string]: TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MetaGuardRemoved = () => void | Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return value of the useMeta api
|
* Return value of the useMeta api
|
||||||
*/
|
*/
|
||||||
export type MetaProxy = {
|
export type MetaProxy = {
|
||||||
meta: MetaSourceProxy
|
meta: MetaSourceProxy
|
||||||
unmount: Function | false
|
onRemoved: (removeGuard: MetaGuardRemoved) => void
|
||||||
|
unmount: (ignoreGuards?: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,17 +52,9 @@ export type MetaResolver = {
|
|||||||
resolve: ResolveMethod
|
resolve: ResolveMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export type MetaResolverSetup = Modify<MetaResolver, {
|
||||||
* The meta manager
|
setup: MetaResolveSetup
|
||||||
*/
|
}>
|
||||||
export type MetaManager = {
|
|
||||||
readonly config: MetaConfig
|
|
||||||
|
|
||||||
install(app: App): void
|
|
||||||
addMeta(source: MetaSource, vm?: ComponentInternalInstance): MetaProxy
|
|
||||||
|
|
||||||
render(ctx?: { slots?: Slots }): Array<VNode>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@@ -66,6 +63,13 @@ export type MetaTeleports = {
|
|||||||
[key: string]: Array<VNode>
|
[key: string]: Array<VNode>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface MetaGuards {
|
||||||
|
removed: Array<MetaGuardRemoved>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@@ -110,5 +114,6 @@ export type MetaRendered = Array<MetaRenderedNode>
|
|||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
interface ComponentInternalInstance {
|
interface ComponentInternalInstance {
|
||||||
$metaManager: MetaManager
|
$metaManager: MetaManager
|
||||||
|
$metaGuards: MetaGuards
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-5
@@ -1,6 +1,8 @@
|
|||||||
import { inject, getCurrentInstance, ComponentInternalInstance } from 'vue'
|
import { inject, getCurrentInstance, ComponentInternalInstance, isProxy, watch } from 'vue'
|
||||||
|
import type { MetaManager } from './manager'
|
||||||
import { metaActiveKey } from './symbols'
|
import { metaActiveKey } from './symbols'
|
||||||
import type { MetaManager, MetaActive, MetaSource, MetaProxy } from './types'
|
import type { MetaActive, MetaSource, MetaProxy } from './types'
|
||||||
|
import { applyDifference } from './utils/diff'
|
||||||
|
|
||||||
export function getCurrentManager (vm?: ComponentInternalInstance): MetaManager | undefined {
|
export function getCurrentManager (vm?: ComponentInternalInstance): MetaManager | undefined {
|
||||||
if (!vm) {
|
if (!vm) {
|
||||||
@@ -15,18 +17,27 @@ export function getCurrentManager (vm?: ComponentInternalInstance): MetaManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useMeta (source: MetaSource, manager?: MetaManager): MetaProxy {
|
export function useMeta (source: MetaSource, manager?: MetaManager): MetaProxy {
|
||||||
const vm = getCurrentInstance()
|
const vm = getCurrentInstance() || undefined
|
||||||
|
|
||||||
if (!manager && vm) {
|
if (!manager && vm) {
|
||||||
manager = getCurrentManager(vm)
|
manager = getCurrentManager(vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
// oopsydoopsy
|
|
||||||
throw new Error('No manager or current instance')
|
throw new Error('No manager or current instance')
|
||||||
}
|
}
|
||||||
|
|
||||||
return manager.addMeta(source, vm || undefined)
|
if (isProxy(source)) {
|
||||||
|
watch(source, (newSource, oldSource) => {
|
||||||
|
// We only care about first level props, second+ level will already be changed by the merge proxy
|
||||||
|
applyDifference(metaProxy.meta, newSource, oldSource)
|
||||||
|
})
|
||||||
|
|
||||||
|
source = source.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaProxy = manager.addMeta(source, vm)
|
||||||
|
return metaProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useActiveMeta (): MetaActive {
|
export function useActiveMeta (): MetaActive {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { isObject } from '@vue/shared'
|
||||||
|
|
||||||
|
type AnyObject = { [key: string] : any }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the differences between newSource & oldSource to target
|
||||||
|
*/
|
||||||
|
export function applyDifference (target: AnyObject, newSource: AnyObject, oldSource: AnyObject) {
|
||||||
|
for (const key in newSource) {
|
||||||
|
if (!(key in oldSource)) {
|
||||||
|
target[key] = newSource[key]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We dont care about nested objects here , these changes
|
||||||
|
// should already have been tracked by the MergeProxy
|
||||||
|
if (isObject(target[key])) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSource[key] !== oldSource[key]) {
|
||||||
|
target[key] = newSource[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in oldSource) {
|
||||||
|
if (!(key in newSource)) {
|
||||||
|
delete target[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user