2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-24 15:10:33 +03:00

feat: make attributes part of the metainfo object

feat: support removing old attributes

feat: better support content as either attribute or child
This commit is contained in:
pimlie
2020-08-02 21:43:55 +02:00
parent 5eaa0ab5b6
commit 5add8bf83f
10 changed files with 163 additions and 92 deletions
+12 -1
View File
@@ -86,8 +86,18 @@ const App = {
] ]
}, },
body: 'body-script1.js', body: 'body-script1.js',
htmlAttrs: {
amp: true,
lang: ['en', 'nl']
},
bodyAttrs: {
class: ['theme-dark']
},
script: [ script: [
{ src: 'head-script1.js' }, { src: 'head-script1.js' },
{ json: { '@context': 'http://schema.org', unsafe: '<p>hello</p>' } },
{ content: 'window.a = "<br/>"; </script><script>alert(\'asdasd\');' },
{ rawContent: 'window.b = "<br/>"; </script><script> alert(\'123321\');' },
{ src: 'body-script2.js', to: 'body' }, { src: 'body-script2.js', to: 'body' },
{ src: 'body-script3.js', to: '#put-it-here' } { src: 'body-script3.js', to: '#put-it-here' }
], ],
@@ -113,6 +123,7 @@ const App = {
}) })
setTimeout(() => (meta.title = 'My Updated Title'), 2000) setTimeout(() => (meta.title = 'My Updated Title'), 2000)
setTimeout(() => (meta.htmlAttrs.amp = undefined), 2000)
const metadata = useMetainfo() const metadata = useMetainfo()
@@ -144,7 +155,7 @@ const App = {
} }
}, },
template: ` template: `
<metainfo :metainfo="metadata" :body-class="'theme-dark'" :html-lang="['en','nl']" html-amp> <metainfo>
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template> <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:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
<template v-slot:og(title)="{ content, metainfo, og }"> <template v-slot:og(title)="{ content, metainfo, og }">
+8 -38
View File
@@ -1,7 +1,7 @@
import { h, watchEffect, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue' import { h, defineComponent, Teleport, VNode, VNodeProps } from 'vue'
import { isArray, isFunction } from '@vue/shared' import { isArray, isFunction } from '@vue/shared'
import { renderMeta } from './render' import { renderMeta } from './render'
import { getCurrentManager } from './useApi' import { useMetainfo, getCurrentManager } from './useApi'
import { MetainfoActive } from './types' import { MetainfoActive } from './types'
export interface MetainfoProps { export interface MetainfoProps {
@@ -24,42 +24,8 @@ export function addVnode (teleports: any, to: string, vnode: VNode | Array<VNode
export const MetainfoImpl = defineComponent({ export const MetainfoImpl = defineComponent({
name: 'Metainfo', name: 'Metainfo',
inheritAttrs: false, inheritAttrs: false,
props: { setup (_, { slots }) {
metainfo: { const metainfo = useMetainfo()
type: Object as PropType<MetainfoActive>,
required: true
}
},
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 () => { return () => {
const teleports: any = {} const teleports: any = {}
@@ -76,6 +42,10 @@ export const MetainfoImpl = defineComponent({
config config
) )
if (!vnodes) {
continue
}
const defaultTo = const defaultTo =
(key !== 'base' && metainfo[key].to) || config.to || 'head' (key !== 'base' && metainfo[key].to) || config.to || 'head'
+11 -17
View File
@@ -1,19 +1,6 @@
export interface ConfigOption { import { Config } from '../types'
tag?: string
to?: string
group?: boolean
keyAttribute?: string
valueAttribute?: string
nameless?: boolean
namespaced?: boolean
namespacedAttribute?: boolean
}
export interface Config { export const defaultConfig: Config = {
[key: string]: ConfigOption
}
const defaultConfig: Config = {
body: { body: {
tag: 'script', tag: 'script',
to: 'body' to: 'body'
@@ -39,7 +26,14 @@ const defaultConfig: Config = {
group: true, group: true,
namespacedAttribute: true, namespacedAttribute: true,
tag: 'meta' tag: 'meta'
},
htmlAttrs: {
attributesFor: 'html'
},
headAttrs: {
attributesFor: 'head'
},
bodyAttrs: {
attributesFor: 'body'
} }
} }
export { defaultConfig }
+2 -2
View File
@@ -1,12 +1,12 @@
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { Config } from './default' import { Config } from '../types'
import { tags } from './tags' import { tags } from './tags'
export function hasConfig (name: string, config: Config): boolean { export function hasConfig (name: string, config: Config): boolean {
return !!config[name] || !!tags[name] return !!config[name] || !!tags[name]
} }
export function getConfigKey ( export function getConfigByKey (
tagOrName: string | Array<string>, tagOrName: string | Array<string>,
key: string, key: string,
config: Config config: Config
+4
View File
@@ -6,6 +6,7 @@
export interface TagConfig { export interface TagConfig {
keyAttribute?: string keyAttribute?: string
contentAsAttribute?: boolean | string
attributes: boolean | Array<string> attributes: boolean | Array<string>
[key: string]: any [key: string]: any
} }
@@ -15,13 +16,16 @@ const tags: { [key: string]: TagConfig } = {
attributes: false attributes: false
}, },
base: { base: {
contentAsAttribute: true,
attributes: ['href', 'target'] attributes: ['href', 'target']
}, },
meta: { meta: {
contentAsAttribute: true,
keyAttribute: 'name', keyAttribute: 'name',
attributes: ['content', 'name', 'http-equiv', 'charset'] attributes: ['content', 'name', 'http-equiv', 'charset']
}, },
link: { link: {
contentAsAttribute: true,
attributes: [ attributes: [
'href', 'href',
'crossorigin', 'crossorigin',
+1
View File
@@ -1,3 +1,4 @@
export { defaultConfig } from './config' export { defaultConfig } from './config'
export { createManager } from './manager' export { createManager } from './manager'
export * from './useApi' export * from './useApi'
export * from './types'
+1 -1
View File
@@ -12,7 +12,7 @@ export function resolveActive (
let value let value
if (shadowParent[key].length > 1) { if (shadowParent[key].length > 1) {
// Is this useful? Idea is to prevent the user from messing with these options by mistake // Is using freeze useful? Idea is to prevent the user from messing with these options by mistake
const getShadow = () => Object.freeze(clone(shadowParent[key])) const getShadow = () => Object.freeze(clone(shadowParent[key]))
const getActive = () => Object.freeze(clone(activeParent[key])) const getActive = () => Object.freeze(clone(activeParent[key]))
+1 -2
View File
@@ -1,9 +1,8 @@
import { App } from 'vue' import { App } from 'vue'
import { isFunction } from '@vue/shared' import { isFunction } from '@vue/shared'
import { Config } from './config'
import { applyMetaPlugin } from './install' import { applyMetaPlugin } from './install'
import * as deepestResolver from './resolvers/deepest' import * as deepestResolver from './resolvers/deepest'
import { ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types' import { Config, ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
export type Manager = { export type Manager = {
readonly config: Config readonly config: Config
+107 -31
View File
@@ -1,8 +1,15 @@
import { h, VNode } from 'vue' import { h, VNode } from 'vue'
import { isArray } from '@vue/shared' import { isArray, isString } from '@vue/shared'
import { getConfigKey } from './config' import { getConfigByKey } from './config'
import { TODO } from './types' import { TODO } from './types'
const cachedElements: {
[key: string]: {
el: Element,
attrs: Array<string>,
}
} = {}
export interface RenderContext { export interface RenderContext {
slots: any slots: any
[key: string]: TODO [key: string]: TODO
@@ -34,9 +41,13 @@ export function renderMeta (
key: string, key: string,
data: TODO, data: TODO,
config: TODO config: TODO
): RenderedMetainfo | RenderedMetainfoNode { ): void | RenderedMetainfo | RenderedMetainfoNode {
console.info('renderMeta', key, data, config) console.info('renderMeta', key, data, config)
if (config.attributesFor) {
return renderAttributes(context, key, data, config)
}
if (config.group) { if (config.group) {
return renderGroup(context, key, data, config) return renderGroup(context, key, data, config)
} }
@@ -92,12 +103,8 @@ export function renderTag (
): RenderedMetainfo | RenderedMetainfoNode { ): RenderedMetainfo | RenderedMetainfoNode {
console.info('renderTag', key, data, config, groupConfig) console.info('renderTag', key, data, config, groupConfig)
/* TODO: not needed I think const contentAttributes = ['content', 'json', 'rawContent']
if (!config.group && isArray(data)) { const getConfig = (key: string) => getConfigByKey([tag, config.tag], key, config)
data = { children: data }
} */
let content, hasChilds
if (isArray(data)) { if (isArray(data)) {
return data return data
@@ -105,7 +112,19 @@ export function renderTag (
return renderTag(context, key, child, config, groupConfig) return renderTag(context, key, child, config, groupConfig)
}) })
.flat() .flat()
}
const { tag = config.tag || key } = data
let content
let hasChilds: boolean = false
let isRaw: boolean = false
if (isString(data)) {
content = data
} else if (data.children && isArray(data.children)) { } else if (data.children && isArray(data.children)) {
hasChilds = true
content = data.children.map((child: string | TODO) => { content = data.children.map((child: string | TODO) => {
const data = renderTag(context, key, child, config, groupConfig) const data = renderTag(context, key, child, config, groupConfig)
@@ -115,12 +134,23 @@ export function renderTag (
return data.vnode return data.vnode
}) })
hasChilds = true
} else { } else {
content = data let i = 0
} for (const contentAttribute of contentAttributes) {
if (!content && data[contentAttribute]) {
if (i === 1) {
content = JSON.stringify(data[contentAttribute])
} else {
content = data[contentAttribute]
}
const { tag = config.tag || key } = data isRaw = i > 1
break
}
i++
}
}
const fullName = (groupConfig && groupConfig.fullName) || key const fullName = (groupConfig && groupConfig.fullName) || key
const slotName = (groupConfig && groupConfig.slotName) || key const slotName = (groupConfig && groupConfig.slotName) || key
@@ -132,33 +162,44 @@ export function renderTag (
delete attributes.tag delete attributes.tag
delete attributes.children delete attributes.children
delete attributes.to delete attributes.to
} else {
// cleanup all content attributes
for (const attr of contentAttributes) {
delete attributes[attr]
}
} else if (!attributes) {
attributes = {} attributes = {}
} }
if (hasChilds) { if (hasChilds) {
content = getSlotContent(context, slotName, content, data) content = getSlotContent(context, slotName, content, data)
} else { } else {
const tagAttributes = getConfigKey([tag, config.tag], 'attributes', config) const contentAsAttribute = getConfig('contentAsAttribute')
console.log('tagAttributes', tagAttributes, config, tag) let valueAttribute = config.valueAttribute
if (tagAttributes) {
if (!valueAttribute && contentAsAttribute) {
const tagAttributes = getConfig('attributes')
valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]
}
if (!valueAttribute) {
content = getSlotContent(context, slotName, content, data)
} else {
if (!config.nameless) { if (!config.nameless) {
const keyAttribute = getConfigKey([tag, config.tag], 'keyAttribute', config) const keyAttribute = getConfig('keyAttribute')
if (keyAttribute) { if (keyAttribute) {
attributes[keyAttribute] = fullName attributes[keyAttribute] = fullName
} }
} }
const valueAttribute = config.valueAttribute || tagAttributes[0]
attributes[valueAttribute] = getSlotContent( attributes[valueAttribute] = getSlotContent(
context, context,
slotName, slotName,
attributes[valueAttribute] || content, attributes[valueAttribute] || content,
groupConfig groupConfig
) )
content = undefined content = undefined
} else {
content = getSlotContent(context, slotName, content, data)
} }
} }
@@ -172,25 +213,60 @@ export function renderTag (
// console.log(' CONTENT', content) // console.log(' CONTENT', content)
// // console.log(data, attributes, config) // // console.log(data, attributes, config)
if (hasChilds) { let vnode
for (const child of content) {
if (child.type === finalTag) {
// TODO: what was this about again?!?!?!?!
return content
}
break if (isRaw) {
} attributes.innerHTML = content
vnode = h(finalTag, attributes)
} else {
vnode = h(finalTag, attributes, content)
} }
const vnode = h(finalTag, attributes, content)
return { return {
to: data.to, to: data.to,
vnode vnode
} }
} }
export function renderAttributes (
context: RenderContext,
key: string,
data: TODO,
config: TODO = {}
): void {
console.info('renderAttributes', key, data, config)
const { attributesFor } = config
if (!cachedElements[attributesFor]) {
const el = document.querySelector(attributesFor)
if (el) {
cachedElements[attributesFor] = {
el,
attrs: []
}
}
}
const { el, attrs } = cachedElements[attributesFor]
for (const attr in data) {
const content = getSlotContent(context, `${key}(${attr})`, data[attr], data)
el.setAttribute(attr, `${content || ''}`)
if (!attrs.includes(attr)) {
attrs.push(attr)
}
}
const attrsToRemove = attrs.filter(attr => !data[attr])
for (const attr of attrsToRemove) {
el.removeAttribute(attr)
}
}
export function getSlotContent ( export function getSlotContent (
{ metainfo, slots }: RenderContext, { metainfo, slots }: RenderContext,
slotName: string, slotName: string,
+16
View File
@@ -8,6 +8,22 @@ export type Immutable<T> = {
export type TODO = any export type TODO = any
export type PathSegments = Array<string> export type PathSegments = Array<string>
export interface ConfigOption {
tag?: string
to?: string
group?: boolean
keyAttribute?: string
valueAttribute?: string
nameless?: boolean
namespaced?: boolean
namespacedAttribute?: boolean
attributesFor?: string
}
export interface Config {
[key: string]: ConfigOption
}
export interface MetainfoInput { export interface MetainfoInput {
[key: string]: TODO [key: string]: TODO
} }