From 5eaa0ab5b63000a56e0f1d4460700cc6a10d3b79 Mon Sep 17 00:00:00 2001 From: pimlie Date: Sun, 26 Jul 2020 23:21:06 +0200 Subject: [PATCH] feat: add support for attributes (wip) support slots for head/body make config fully external (while providing defaults users can use) type fixes --- examples/vue-router/app.js | 59 ++++++++++++++++++++++-------- src/Metainfo.ts | 56 ++++++++++++++++++++++++----- src/config.ts | 74 -------------------------------------- src/config/default.ts | 45 +++++++++++++++++++++++ src/config/helpers.ts | 32 +++++++++++++++++ src/config/index.ts | 3 ++ src/index.ts | 1 + src/manager.ts | 24 ++++--------- src/types/index.ts | 5 +++ src/useApi.ts | 5 +-- test/unit/proxy.test.js | 0 11 files changed, 186 insertions(+), 118 deletions(-) delete mode 100644 src/config.ts create mode 100644 src/config/default.ts create mode 100644 src/config/helpers.ts create mode 100644 src/config/index.ts create mode 100644 test/unit/proxy.test.js diff --git a/examples/vue-router/app.js b/examples/vue-router/app.js index 43417fa..c81c9c0 100644 --- a/examples/vue-router/app.js +++ b/examples/vue-router/app.js @@ -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: [ - //'', { tag: 'link', rel: 'stylesheet', href: 'style.css' } ], otherNoscript: { tag: 'noscript', 'data-test': 'hello', children: [ - //'', { tag: 'link', rel: 'stylesheet', href: 'style2.css' } ] }, body: 'body-script1.js', script: [ - //'', { 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: ` - + + + + + + + +
@@ -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( { diff --git a/src/Metainfo.ts b/src/Metainfo.ts index 9156cde..91309b2 100644 --- a/src/Metainfo.ts +++ b/src/Metainfo.ts @@ -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) { 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, 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]) }) diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index e0c5dc8..0000000 --- a/src/config.ts +++ /dev/null @@ -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, - 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] - } -} diff --git a/src/config/default.ts b/src/config/default.ts new file mode 100644 index 0000000..3013e30 --- /dev/null +++ b/src/config/default.ts @@ -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 } diff --git a/src/config/helpers.ts b/src/config/helpers.ts new file mode 100644 index 0000000..96ceb87 --- /dev/null +++ b/src/config/helpers.ts @@ -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, + 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] + } +} diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..ca7c14e --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,3 @@ +export * from './default' +export * from './helpers' +export * from './tags' diff --git a/src/index.ts b/src/index.ts index 5b684f3..cbd8471 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ +export { defaultConfig } from './config' export { createManager } from './manager' export * from './useApi' diff --git a/src/manager.ts b/src/manager.ts index 5cdc525..0bb2af4 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -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) } diff --git a/src/types/index.ts b/src/types/index.ts index 88d4a1b..d5e8aac 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,6 +35,11 @@ export interface ActiveResolverObject { resolve: ActiveResolverMethod } +export interface ManagerResolverObject { + setup: ActiveResolverSetup + resolve: ActiveResolverMethod +} + export interface ShadowNode { [key: string]: TODO } diff --git a/src/useApi.ts b/src/useApi.ts index d4725b3..b2c21b6 100644 --- a/src/useApi.ts +++ b/src/useApi.ts @@ -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) diff --git a/test/unit/proxy.test.js b/test/unit/proxy.test.js new file mode 100644 index 0000000..e69de29