2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-11 21:52:24 +03:00

feat: add support for computed metadata

feat: show active metadata and add some styling

chore: code cleanup
This commit is contained in:
pimlie
2021-02-28 22:58:18 +01:00
parent 498d747301
commit 3e1a0da9e4
12 changed files with 876 additions and 327 deletions
+44 -6
View File
@@ -7,16 +7,54 @@ html, body {
padding: 0 20px;
}
ul {
line-height: 1.5em;
padding-left: 1.5em;
}
a {
color: #7f8c8d;
color: #2c3e50;
text-decoration: none;
}
a:hover {
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;
}
+9 -4
View File
@@ -177,16 +177,21 @@ export default {
<div id="app">
<h1>vue-router</h1>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<ul class="menu">
<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">
<component :is="Component" />
</transition>
</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>
`
}
+27 -22
View File
@@ -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'
const metaUpdated = 'no' // TODO: afterNavigation hook?
let metaUpdated = 'no'
export default defineComponent({
name: 'ChildComponent',
props: {
page: {
type: String,
required: true
}
},
setup (props) {
setup () {
const route = useRoute()
const state = reactive({
date: null,
metaUpdated
})
const title = props.page[0].toUpperCase() + props.page.slice(1)
console.log('ChildComponent Setup')
useMeta({
const metaConfig = computed(() => ({
charset: 'utf16',
title,
description: 'Description ' + props.page,
title: route.name[0].toUpperCase() + route.name.slice(1),
description: 'Description ' + route.name,
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: `
<div>
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
template: `<div>
<h3>You're looking at the <strong>{{ pageName }}</strong> page</h3>
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
</div>
`
</div>`
})
+5 -9
View File
@@ -4,12 +4,9 @@ import { createMetaManager, defaultConfig, resolveOption, useMeta } from 'vue-me
import App from './App'
import ChildComponent from './Child'
function createView (page) {
function createComponent () {
return {
name: `section-${page}`,
render () {
return h(ChildComponent, { page })
}
render: () => h(ChildComponent)
}
}
@@ -41,14 +38,13 @@ useMeta(
const createRouter = (base, isSSR) => createVueRouter({
history: isSSR ? createMemoryHistory(base) : createWebHistory(base),
routes: [
{ name: 'home', path: '/', component: createView('home') },
{ name: 'about', path: '/about', component: createView('about') }
{ name: 'home', path: '/', component: createComponent() },
{ name: 'about', path: '/about', component: createComponent() }
]
})
export {
App,
metaManager,
createRouter,
createView
createRouter
}
+23 -21
View File
@@ -53,36 +53,37 @@
"vue": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/preset-typescript": "^7.12.7",
"@babel/core": "^7.12.16",
"@babel/plugin-transform-modules-commonjs": "^7.12.13",
"@babel/preset-typescript": "^7.12.16",
"@nuxtjs/eslint-config-typescript": "^5.0.0",
"@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.1.0",
"@rollup/plugin-alias": "^3.1.2",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"@rollup/plugin-replace": "^2.3.4",
"@types/webpack": "^4.41.26",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.15.0",
"@vue/compiler-sfc": "^3.0.5",
"@vue/server-renderer": "^3.0.5",
"@vue/server-test-utils": "^1.1.2",
"@vue/test-utils": "^1.1.2",
"@vue/server-test-utils": "^1.1.3",
"@vue/test-utils": "^1.1.3",
"@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-global-define": "^1.0.3",
"babel-plugin-module-resolver": "^4.1.0",
"babel-preset-vue": "^2.0.2",
"browserstack-local": "^1.4.8",
"chromedriver": "^88.0.0",
"codecov": "^3.8.1",
"consola": "^2.15.0",
"eslint": "^7.18.0",
"consola": "^2.15.3",
"eslint": "^7.20.0",
"express-urlrewrite": "^1.4.0",
"geckodriver": "^1.21.1",
"html-webpack-plugin": "^4.5.1",
"geckodriver": "^1.22.1",
"html-webpack-plugin": "^5.1.0",
"jest": "^26.6.3",
"jest-environment-jsdom": "^26.6.2",
"jest-environment-jsdom-global": "^2.0.4",
@@ -90,25 +91,26 @@
"jsdom": "^16.4.0",
"lodash": "^4.17.20",
"node-env-file": "^0.1.8",
"puppeteer-core": "^5.5.0",
"puppeteer-core": "^7.1.0",
"rimraf": "^3.0.2",
"rollup": "^2.38.0",
"rollup": "^2.39.0",
"rollup-plugin-dts": "^2.0.1",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.29.0",
"selenium-webdriver": "^4.0.0-alpha.8",
"standard-version": "^9.1.0",
"tib": "^0.7.5",
"ts-jest": "^26.4.4",
"ts-loader": "^8.0.14",
"typescript": "^4.1.3",
"ts-jest": "^26.5.1",
"ts-loader": "^8.0.17",
"typescript": "^4.1.5",
"vite": "^2.0.4",
"vue": "^3.0.5",
"vue-jest": "^3.0.7",
"vue-loader": "^16.0.0",
"vue-router": "next",
"webpack": "^5.17.0",
"webpack": "^5.21.2",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.4.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpackbar": "^4.0.0"
}
+171 -102
View File
@@ -1,7 +1,7 @@
import { h, reactive, onUnmounted, Teleport, Comment, getCurrentInstance } from 'vue'
import type { VNode } from 'vue'
import { h, reactive, onUnmounted, App, Teleport, Comment, getCurrentInstance, ComponentInternalInstance, Slots } from 'vue'
import type { VNode, ComponentPublicInstance } from 'vue'
import { isArray, isFunction } from '@vue/shared'
import { createMergedObject } from './object-merge'
import { createMergedObject, MergedObjectBuilder } from './object-merge'
import { renderMeta } from './render'
import { metaActiveKey } from './symbols'
import { Metainfo } from './Metainfo'
@@ -9,10 +9,14 @@ import type { ResolveMethod } from './object-merge'
import type {
MetaActive,
MetaConfig,
MetaManager,
MetaGuards,
MetaGuardRemoved,
MetaResolver,
MetaResolveContext,
MetaTeleports
MetaTeleports,
MetaSource,
MetaResolverSetup,
MetaProxy
} from './types'
export const ssrAttribute = 'data-vm-ssr'
@@ -46,127 +50,192 @@ export function addVnode (teleports: MetaTeleports, to: string, vnodes: VNode |
teleports[to].push(...nodes)
}
export function createMetaManager (config: MetaConfig, resolver: MetaResolver | ResolveMethod): MetaManager {
let cleanedUpSsr = false
// eslint-disable-next-line no-use-before-define
export type createMetaManagerMethod = (config: MetaConfig, resolver: MetaResolver | ResolveMethod) => MetaManager
const resolve: ResolveMethod = (options, contexts, active, key, pathSegments) => {
if (isFunction(resolver)) {
return resolver(options, contexts, active, key, pathSegments)
export const createMetaManager: createMetaManagerMethod = (config, resolver) => MetaManager.create(config, resolver)
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)
// TODO: validate resolver
const manager: MetaManager = {
config,
install (app) {
app.component('Metainfo', Metainfo)
app.config.globalProperties.$metaManager = manager
app.provide(metaActiveKey, active)
},
addMeta (metaObj, vm) {
if (!vm) {
vm = getCurrentInstance() || undefined
static create: createMetaManagerMethod = (config, resolver) => {
const resolve: ResolveMethod = (options, contexts, active, key, pathSegments) => {
if (isFunction(resolver)) {
return resolver(options, contexts, active, key, pathSegments)
}
const resolveContext: MetaResolveContext = { vm }
if (resolver && 'setup' in resolver && isFunction(resolver.setup)) {
resolver.setup(resolveContext)
}
return resolver.resolve(options, contexts, active, key, pathSegments)
}
// TODO: optimize initial compute
const meta = addSource(metaObj, resolveContext, true)
const mergedObject = createMergedObject(resolve, active)
const unmount = () => delSource(meta)
if (vm) {
onUnmounted(unmount)
}
// TODO: validate resolver
const manager = new MetaManager(config, mergedObject, resolver)
return manager
}
return {
meta,
unmount
}
},
install (app: App): void {
app.component('Metainfo', Metainfo)
render ({ slots } = {}) {
// cleanup ssr tags if not yet done
if (__BROWSER__ && !cleanedUpSsr) {
cleanedUpSsr = true
app.config.globalProperties.$metaManager = this
app.provide(metaActiveKey, active)
}
// 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}]`)
addMeta (metadata: MetaSource, vm?: ComponentInternalInstance): MetaProxy {
if (!vm) {
vm = getCurrentInstance() || undefined
}
if (ssrTags && ssrTags.length) {
Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el))
const metaGuards: MetaGuards = ({
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) {
const config = this.config[key] || {}
let defaultTo = key !== 'base' && active[key].to
let renderedNodes = renderMeta(
{ metainfo: active, slots },
key,
active[key],
config
)
if (!defaultTo && 'to' in config) {
defaultTo = config.to
}
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
}
if (!isArray(renderedNodes)) {
renderedNodes = [renderedNodes]
}
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)
const slot = slots[slotName]
if (isFunction(slot)) {
addVnode(teleports, tagName, slot({ metainfo: active }))
}
}
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
View File
@@ -1,5 +1,5 @@
import { h } from 'vue'
import { isArray, isString } from '@vue/shared'
import { isArray, isFunction, isString } from '@vue/shared'
import { getTagConfigItem } from './config'
import type {
MetaConfigSectionAttribute,
@@ -279,7 +279,7 @@ export function getSlotContent (
groupConfig?: MetaGroupConfig
): string {
const slot = slots && slots[slotName]
if (!slot) {
if (!slot || !isFunction(slot)) {
return content
}
+1 -1
View File
@@ -10,5 +10,5 @@ export const PolySymbol = (name: string) =>
: (__DEV__ ? '[vue-meta]: ' : '_vm_') + name
export const metaActiveKey = /*#__PURE__*/ PolySymbol(
__DEV__ ? 'active_meta' : 'am'
__DEV__ ? 'meta_active' : 'ma'
) as InjectionKey<MetaActive>
+19 -14
View File
@@ -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 { MetaConfig } from './config'
import type { MetaManager } from '../manager'
export * from './config'
export type Modify<T, R> = Omit<T, keyof R> & R;
export type TODO = any
/**
@@ -18,12 +20,15 @@ export type MetaSource = {
[key: string]: TODO
}
export type MetaGuardRemoved = () => void | Promise<void>
/**
* Return value of the useMeta api
*/
export type MetaProxy = {
meta: MetaSourceProxy
unmount: Function | false
onRemoved: (removeGuard: MetaGuardRemoved) => void
unmount: (ignoreGuards?: boolean) => void
}
/**
@@ -47,17 +52,9 @@ export type MetaResolver = {
resolve: ResolveMethod
}
/**
* The meta manager
*/
export type MetaManager = {
readonly config: MetaConfig
install(app: App): void
addMeta(source: MetaSource, vm?: ComponentInternalInstance): MetaProxy
render(ctx?: { slots?: Slots }): Array<VNode>
}
export type MetaResolverSetup = Modify<MetaResolver, {
setup: MetaResolveSetup
}>
/**
* @internal
@@ -66,6 +63,13 @@ export type MetaTeleports = {
[key: string]: Array<VNode>
}
/**
* @internal
*/
export interface MetaGuards {
removed: Array<MetaGuardRemoved>
}
/**
* @internal
*/
@@ -110,5 +114,6 @@ export type MetaRendered = Array<MetaRenderedNode>
declare module '@vue/runtime-core' {
interface ComponentInternalInstance {
$metaManager: MetaManager
$metaGuards: MetaGuards
}
}
+16 -5
View File
@@ -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 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 {
if (!vm) {
@@ -15,18 +17,27 @@ export function getCurrentManager (vm?: ComponentInternalInstance): MetaManager
}
export function useMeta (source: MetaSource, manager?: MetaManager): MetaProxy {
const vm = getCurrentInstance()
const vm = getCurrentInstance() || undefined
if (!manager && vm) {
manager = getCurrentManager(vm)
}
if (!manager) {
// oopsydoopsy
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 {
+31
View File
@@ -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]
}
}
}
+528 -141
View File
File diff suppressed because it is too large Load Diff