2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-17 16:00:34 +03:00

feat: add support for attributes (wip)

support slots for head/body

make config fully external (while providing defaults users can use)

type fixes
This commit is contained in:
pimlie
2020-07-26 23:21:06 +02:00
parent 27aaf4744a
commit 5eaa0ab5b6
11 changed files with 186 additions and 118 deletions
+44 -15
View File
@@ -9,7 +9,7 @@ import {
watch
} from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createManager, useMeta, useMetainfo } from 'vue-meta'
import { defaultConfig, createManager, useMeta, useMetainfo } from 'vue-meta'
// import About from './about.vue'
const metaUpdated = 'no'
@@ -76,22 +76,18 @@ const App = {
title: 'Twitter Title'
},
noscript: [
//'<!-- // A code comment -->',
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }
],
otherNoscript: {
tag: 'noscript',
'data-test': 'hello',
children: [
//'<!-- // Another code comment -->',
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
]
},
body: 'body-script1.js',
script: [
//'<!--[if IE]>',
{ src: 'head-script1.js' },
//'<![endif]-->',
{ src: 'body-script2.js', to: 'body' },
{ src: 'body-script3.js', to: '#put-it-here' }
],
@@ -120,23 +116,58 @@ const App = {
const metadata = useMetainfo()
window.$metainfo = metadata
window.$metadata = metadata
watch(metadata, (newValue, oldValue) => {
console.log('UPDATE', newValue)
})
/* let i = 0
const walk = (data, path = []) => {
for (const key in data) {
const newPath = [...path, key]
if (typeof data[key] === 'object') {
walk(data[key], newPath)
} else {
console.log(newPath.join('.'))
}
i++
if (i > 50) {
break
}
}
}
setTimeout(() => walk(metadata), 1000) */
return {
metadata
}
},
template: `
<metainfo :metainfo="metadata">
<metainfo :metainfo="metadata" :body-class="'theme-dark'" :html-lang="['en','nl']" html-amp>
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
<template v-slot:og(title)="{ content, metainfo, og }">
{{ content }} - {{ og.description }} - {{ metainfo.description }} - Hello Again
</template>
<script src="lalala1.js"></script>
<script src="lalala2.js"></script>
<template v-slot:head="{ metainfo }">
<!--[if IE]>
// -> Reactivity is not supported by Vue, all comments are ignored
<script :src="metainfo.script[0].src" ></script>
// -> but a static file should work
<script src="lalala3.js" ></script>
// -> altho Vue probably strips comments in production builds (but can be configged afaik)
<![endif]-->
<script :src="metainfo.script[0].src" ></script>
</template>
<template v-slot:body>
<script src="lalala4.js"></script>
</template>
</metainfo>
<div id="app">
@@ -172,15 +203,13 @@ function decisionMaker5000000 (key, pathSegments, getOptions, getCurrentValue) {
}
const metaManager = createManager({
resolver: decisionMaker5000000,
config: {
esi: {
group: true,
namespaced: true,
attributes: ['src', 'test', 'text']
}
...defaultConfig,
esi: {
group: true,
namespaced: true,
attributes: ['src', 'test', 'text']
}
})
}, decisionMaker5000000)
/* useMeta(
{
+48 -8
View File
@@ -1,5 +1,5 @@
import { h, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
import { isArray } from '@vue/shared'
import { h, watchEffect, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
import { isArray, isFunction } from '@vue/shared'
import { renderMeta } from './render'
import { getCurrentManager } from './useApi'
import { MetainfoActive } from './types'
@@ -8,23 +8,59 @@ export interface MetainfoProps {
metainfo: MetainfoActive
}
export function addVnode (teleports: any, to: string, vnode: VNode) {
export function addVnode (teleports: any, to: string, vnode: VNode | Array<VNode>) {
if (!teleports[to]) {
teleports[to] = []
}
if (isArray(vnode)) {
teleports[to].push(...vnode)
return
}
teleports[to].push(vnode)
}
export const MetainfoImpl = defineComponent({
name: 'Metainfo',
inheritAttrs: false,
props: {
metainfo: {
type: Object as PropType<MetainfoActive>,
required: true
}
},
setup ({ metainfo }, { slots }) {
setup ({ metainfo }, { attrs, slots }) {
const tags: { [key: string]: Element } = {}
watchEffect(() => {
const attributes = Object.keys(attrs)
for (const tagName of ['html', 'head', 'body']) {
const tagAttrs = attributes.filter(attr => attr.startsWith(tagName + '-'))
if (!tagAttrs.length) {
continue
}
if (!tags[tagName]) {
const foundTag = document.querySelector(tagName)
if (foundTag) {
tags[tagName] = foundTag
}
}
const tag: Element = tags[tagName]
for (const tagAttr of tagAttrs) {
const attr: string = tagAttr.slice(5)
tag.setAttribute(attr, `${attrs[tagAttr] || ''}`)
}
}
})
return () => {
const teleports: any = {}
@@ -39,24 +75,28 @@ export const MetainfoImpl = defineComponent({
metainfo[key],
config
)
console.log('RENDERED VNODES', vnodes)
const defaultTo =
(key !== 'base' && metainfo[key].to) || config.to || 'head'
if (isArray(vnodes)) {
for (const { to, vnode } of vnodes) {
console.log('VNODE 1', vnode)
addVnode(teleports, to || defaultTo, vnode)
}
continue
}
const { to, vnode } = vnodes
console.log('VNODE 2', vnode)
addVnode(teleports, to || defaultTo, vnode)
}
console.log('TARGETS', teleports)
for (const tag of ['default', 'head', 'body']) {
const slotFn = slots[tag]
if (isFunction(slotFn)) {
addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo }))
}
}
return Object.keys(teleports).map((to) => {
return h(Teleport, { to }, teleports[to])
})
-74
View File
@@ -1,74 +0,0 @@
import { isArray } from '@vue/shared'
import { tags } from './config/tags'
import { TODO } from './types'
export interface ConfigOption {
tag?: string
to?: string
group?: boolean
keyAttribute?: string
valueAttribute?: string
nameless?: boolean
namespaced?: boolean
namespacedAttribute?: boolean
}
const defaultMapping: { [key: string]: ConfigOption } = {
body: {
tag: 'script',
to: 'body'
},
base: {
valueAttribute: 'href'
},
charset: {
tag: 'meta',
nameless: true,
valueAttribute: 'charset'
},
description: {
tag: 'meta'
},
og: {
group: true,
namespacedAttribute: true,
tag: 'meta',
keyAttribute: 'property'
},
twitter: {
group: true,
namespacedAttribute: true,
tag: 'meta'
}
}
export { defaultMapping }
export function hasConfig (name: string): boolean {
return !!tags[name] || !!defaultMapping[name]
}
export function getConfigKey (
tagOrName: string | Array<string>,
key: string,
config: TODO
): any {
if (config && key in config) {
return config[key]
}
if (isArray(tagOrName)) {
for (const name of tagOrName) {
if (name && name in tags) {
return tags[name][key]
}
}
return
}
if (tagOrName in tags) {
const tag = tags[tagOrName]
return tag[key]
}
}
+45
View File
@@ -0,0 +1,45 @@
export interface ConfigOption {
tag?: string
to?: string
group?: boolean
keyAttribute?: string
valueAttribute?: string
nameless?: boolean
namespaced?: boolean
namespacedAttribute?: boolean
}
export interface Config {
[key: string]: ConfigOption
}
const defaultConfig: Config = {
body: {
tag: 'script',
to: 'body'
},
base: {
valueAttribute: 'href'
},
charset: {
tag: 'meta',
nameless: true,
valueAttribute: 'charset'
},
description: {
tag: 'meta'
},
og: {
group: true,
namespacedAttribute: true,
tag: 'meta',
keyAttribute: 'property'
},
twitter: {
group: true,
namespacedAttribute: true,
tag: 'meta'
}
}
export { defaultConfig }
+32
View File
@@ -0,0 +1,32 @@
import { isArray } from '@vue/shared'
import { Config } from './default'
import { tags } from './tags'
export function hasConfig (name: string, config: Config): boolean {
return !!config[name] || !!tags[name]
}
export function getConfigKey (
tagOrName: string | Array<string>,
key: string,
config: Config
): any {
if (config && key in config) {
return config[key]
}
if (isArray(tagOrName)) {
for (const name of tagOrName) {
if (name && name in tags) {
return tags[name][key]
}
}
return
}
if (tagOrName in tags) {
const tag = tags[tagOrName]
return tag[key]
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './default'
export * from './helpers'
export * from './tags'
+1
View File
@@ -1,2 +1,3 @@
export { defaultConfig } from './config'
export { createManager } from './manager'
export * from './useApi'
+7 -17
View File
@@ -1,30 +1,23 @@
import { App } from 'vue'
import { isFunction } from '@vue/shared'
import { defaultMapping } from './config'
import { Config } from './config'
import { applyMetaPlugin } from './install'
import * as deepestResolver from './resolvers/deepest'
import { TODO, ActiveResolverObject, MetaContext, PathSegments } from './types'
export type ManagerOptions = {
install(app: App): void
}
import { ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
export type Manager = {
readonly config: TODO
readonly config: Config
resolver: ActiveResolverObject
resolver: ManagerResolverObject
install(app: App): void
}
export function createManager (options: TODO = {}): Manager {
const { resolver = deepestResolver } = options
export function createManager (config: Config, resolver: ActiveResolverObject = deepestResolver): Manager {
// TODO: validate resolver
const manager: Manager = {
resolver: {
setup (context: MetaContext) {
if (!resolver || isFunction(resolver)) {
if (!resolver || !resolver.setup || isFunction(resolver)) {
return
}
@@ -42,10 +35,7 @@ export function createManager (options: TODO = {}): Manager {
return resolver.resolve(key, pathSegments, getShadow, getActive)
}
},
config: {
...defaultMapping,
...options.config
},
config,
install (app: App) {
applyMetaPlugin(app, this)
}
+5
View File
@@ -35,6 +35,11 @@ export interface ActiveResolverObject {
resolve: ActiveResolverMethod
}
export interface ManagerResolverObject {
setup: ActiveResolverSetup
resolve: ActiveResolverMethod
}
export interface ShadowNode {
[key: string]: TODO
}
+1 -4
View File
@@ -10,7 +10,6 @@ let contextCounter: number = 0
export function useMeta (obj: MetainfoInput, manager?: Manager) {
const vm = getCurrentInstance()
if (vm) {
console.log(vm)
manager = vm.appContext.config.globalProperties.$metaManager
}
@@ -30,9 +29,7 @@ export function useMeta (obj: MetainfoInput, manager?: Manager) {
onUnmounted(unmount)
}
if (manager.resolver.setup) {
manager.resolver.setup(context)
}
manager.resolver.setup(context)
setByObject(context, obj)
View File