2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-14 21:52:24 +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',
htmlAttrs: {
amp: true,
lang: ['en', 'nl']
},
bodyAttrs: {
class: ['theme-dark']
},
script: [
{ 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-script3.js', to: '#put-it-here' }
],
@@ -113,6 +123,7 @@ const App = {
})
setTimeout(() => (meta.title = 'My Updated Title'), 2000)
setTimeout(() => (meta.htmlAttrs.amp = undefined), 2000)
const metadata = useMetainfo()
@@ -144,7 +155,7 @@ const App = {
}
},
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:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
<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 { renderMeta } from './render'
import { getCurrentManager } from './useApi'
import { useMetainfo, getCurrentManager } from './useApi'
import { MetainfoActive } from './types'
export interface MetainfoProps {
@@ -24,42 +24,8 @@ export function addVnode (teleports: any, to: string, vnode: VNode | Array<VNode
export const MetainfoImpl = defineComponent({
name: 'Metainfo',
inheritAttrs: false,
props: {
metainfo: {
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] || ''}`)
}
}
})
setup (_, { slots }) {
const metainfo = useMetainfo()
return () => {
const teleports: any = {}
@@ -76,6 +42,10 @@ export const MetainfoImpl = defineComponent({
config
)
if (!vnodes) {
continue
}
const defaultTo =
(key !== 'base' && metainfo[key].to) || config.to || 'head'
+11 -17
View File
@@ -1,19 +1,6 @@
export interface ConfigOption {
tag?: string
to?: string
group?: boolean
keyAttribute?: string
valueAttribute?: string
nameless?: boolean
namespaced?: boolean
namespacedAttribute?: boolean
}
import { Config } from '../types'
export interface Config {
[key: string]: ConfigOption
}
const defaultConfig: Config = {
export const defaultConfig: Config = {
body: {
tag: 'script',
to: 'body'
@@ -39,7 +26,14 @@ const defaultConfig: Config = {
group: true,
namespacedAttribute: true,
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 { Config } from './default'
import { Config } from '../types'
import { tags } from './tags'
export function hasConfig (name: string, config: Config): boolean {
return !!config[name] || !!tags[name]
}
export function getConfigKey (
export function getConfigByKey (
tagOrName: string | Array<string>,
key: string,
config: Config
+4
View File
@@ -6,6 +6,7 @@
export interface TagConfig {
keyAttribute?: string
contentAsAttribute?: boolean | string
attributes: boolean | Array<string>
[key: string]: any
}
@@ -15,13 +16,16 @@ const tags: { [key: string]: TagConfig } = {
attributes: false
},
base: {
contentAsAttribute: true,
attributes: ['href', 'target']
},
meta: {
contentAsAttribute: true,
keyAttribute: 'name',
attributes: ['content', 'name', 'http-equiv', 'charset']
},
link: {
contentAsAttribute: true,
attributes: [
'href',
'crossorigin',
+1
View File
@@ -1,3 +1,4 @@
export { defaultConfig } from './config'
export { createManager } from './manager'
export * from './useApi'
export * from './types'
+1 -1
View File
@@ -12,7 +12,7 @@ export function resolveActive (
let value
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 getActive = () => Object.freeze(clone(activeParent[key]))
+1 -2
View File
@@ -1,9 +1,8 @@
import { App } from 'vue'
import { isFunction } from '@vue/shared'
import { Config } from './config'
import { applyMetaPlugin } from './install'
import * as deepestResolver from './resolvers/deepest'
import { ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
import { Config, ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
export type Manager = {
readonly config: Config
+107 -31
View File
@@ -1,8 +1,15 @@
import { h, VNode } from 'vue'
import { isArray } from '@vue/shared'
import { getConfigKey } from './config'
import { isArray, isString } from '@vue/shared'
import { getConfigByKey } from './config'
import { TODO } from './types'
const cachedElements: {
[key: string]: {
el: Element,
attrs: Array<string>,
}
} = {}
export interface RenderContext {
slots: any
[key: string]: TODO
@@ -34,9 +41,13 @@ export function renderMeta (
key: string,
data: TODO,
config: TODO
): RenderedMetainfo | RenderedMetainfoNode {
): void | RenderedMetainfo | RenderedMetainfoNode {
console.info('renderMeta', key, data, config)
if (config.attributesFor) {
return renderAttributes(context, key, data, config)
}
if (config.group) {
return renderGroup(context, key, data, config)
}
@@ -92,12 +103,8 @@ export function renderTag (
): RenderedMetainfo | RenderedMetainfoNode {
console.info('renderTag', key, data, config, groupConfig)
/* TODO: not needed I think
if (!config.group && isArray(data)) {
data = { children: data }
} */
let content, hasChilds
const contentAttributes = ['content', 'json', 'rawContent']
const getConfig = (key: string) => getConfigByKey([tag, config.tag], key, config)
if (isArray(data)) {
return data
@@ -105,7 +112,19 @@ export function renderTag (
return renderTag(context, key, child, config, groupConfig)
})
.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)) {
hasChilds = true
content = data.children.map((child: string | TODO) => {
const data = renderTag(context, key, child, config, groupConfig)
@@ -115,12 +134,23 @@ export function renderTag (
return data.vnode
})
hasChilds = true
} 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 slotName = (groupConfig && groupConfig.slotName) || key
@@ -132,33 +162,44 @@ export function renderTag (
delete attributes.tag
delete attributes.children
delete attributes.to
} else {
// cleanup all content attributes
for (const attr of contentAttributes) {
delete attributes[attr]
}
} else if (!attributes) {
attributes = {}
}
if (hasChilds) {
content = getSlotContent(context, slotName, content, data)
} else {
const tagAttributes = getConfigKey([tag, config.tag], 'attributes', config)
console.log('tagAttributes', tagAttributes, config, tag)
if (tagAttributes) {
const contentAsAttribute = getConfig('contentAsAttribute')
let valueAttribute = config.valueAttribute
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) {
const keyAttribute = getConfigKey([tag, config.tag], 'keyAttribute', config)
const keyAttribute = getConfig('keyAttribute')
if (keyAttribute) {
attributes[keyAttribute] = fullName
}
}
const valueAttribute = config.valueAttribute || tagAttributes[0]
attributes[valueAttribute] = getSlotContent(
context,
slotName,
attributes[valueAttribute] || content,
groupConfig
)
content = undefined
} else {
content = getSlotContent(context, slotName, content, data)
}
}
@@ -172,25 +213,60 @@ export function renderTag (
// console.log(' CONTENT', content)
// // console.log(data, attributes, config)
if (hasChilds) {
for (const child of content) {
if (child.type === finalTag) {
// TODO: what was this about again?!?!?!?!
return content
}
let vnode
break
}
if (isRaw) {
attributes.innerHTML = content
vnode = h(finalTag, attributes)
} else {
vnode = h(finalTag, attributes, content)
}
const vnode = h(finalTag, attributes, content)
return {
to: data.to,
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 (
{ metainfo, slots }: RenderContext,
slotName: string,
+16
View File
@@ -8,6 +8,22 @@ export type Immutable<T> = {
export type TODO = any
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 {
[key: string]: TODO
}