From a8d6f0ab0ea3cf4f3c3fbdc6993693569eb3725d Mon Sep 17 00:00:00 2001 From: pimlie Date: Mon, 17 May 2021 00:12:16 +0000 Subject: [PATCH] chore(release): 3.0.0-alpha.6 --- CHANGELOG.md | 16 ++++ dist/vue-meta-ssr.d.ts | 6 ++ dist/vue-meta.cjs.js | 156 +++++++++++++++++++++++------- dist/vue-meta.cjs.prod.js | 143 +++++++++++++++++++++------- dist/vue-meta.d.ts | 37 +++++--- dist/vue-meta.esm-browser.js | 126 ++++++++++++++++-------- dist/vue-meta.esm-browser.min.js | 4 +- dist/vue-meta.esm-bundler.js | 158 +++++++++++++++++++++++-------- dist/vue-meta.global.js | 126 ++++++++++++++++-------- dist/vue-meta.global.min.js | 4 +- package.json | 2 +- 11 files changed, 571 insertions(+), 207 deletions(-) create mode 100644 dist/vue-meta-ssr.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ee06c..4611992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.0.0-alpha.6](https://github.com/nuxt/vue-meta/compare/v3.0.0-alpha.5...v3.0.0-alpha.6) (2021-05-17) + + +### Features + +* add support for recomputing nested paths ([8c0fb63](https://github.com/nuxt/vue-meta/commit/8c0fb63f123151395d0c7afcffe2af1869a71e41)) + + +### Bug Fixes + +* also delete previous values if computed value was faly ([449bb20](https://github.com/nuxt/vue-meta/commit/449bb20e6f42d0ddb837c2ba8e7d8197e887f205)) +* better ssr support ([1d84787](https://github.com/nuxt/vue-meta/commit/1d847870e949ea7dac710153361185cc75ed704a)) +* make types of deepest resolver compatible ([d8651be](https://github.com/nuxt/vue-meta/commit/d8651be35bd288f5e6016121b6ee384ba190afd9)) +* recompute all props when assigning an object to a proxy key ([cae8e35](https://github.com/nuxt/vue-meta/commit/cae8e35340d17b72c6da4283d0043274c5dc53d3)) +* rollup config, esm-bundler builds are also browser builds ([d7be9a4](https://github.com/nuxt/vue-meta/commit/d7be9a43e517910357e6232e0572a05b78e5ab5d)) + ## [3.0.0-alpha.5](https://github.com/nuxt/vue-meta/compare/v3.0.0-alpha.4...v3.0.0-alpha.5) (2021-05-03) diff --git a/dist/vue-meta-ssr.d.ts b/dist/vue-meta-ssr.d.ts new file mode 100644 index 0000000..70d53b7 --- /dev/null +++ b/dist/vue-meta-ssr.d.ts @@ -0,0 +1,6 @@ +import { App } from 'vue'; +import { SSRContext } from '@vue/server-renderer'; + +declare function renderMetaToString(app: App, ctx?: SSRContext): Promise; + +export { renderMetaToString }; diff --git a/dist/vue-meta.cjs.js b/dist/vue-meta.cjs.js index 2c8b6b2..72cb0f5 100644 --- a/dist/vue-meta.cjs.js +++ b/dist/vue-meta.cjs.js @@ -1,5 +1,5 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors @@ -27,7 +27,7 @@ const resolveOption = (predicament, initialValue) => (options, contexts) => { } }; -function setup(context) { +const setup = (context) => { let depth = 0; if (context.vm) { let { vm } = context; @@ -39,7 +39,7 @@ function setup(context) { } while (vm && vm.parent && vm !== vm.root); } context.depth = depth; -} +}; const resolve = resolveOption((currentValue, context) => { const { depth } = context; if (!currentValue || depth > currentValue) { @@ -203,7 +203,7 @@ function clone(v) { const pluck = (collection, key, callback) => { const plucked = []; for (const row of collection) { - if (key in row) { + if (row && key in row) { plucked.push(row[key]); if (callback) { callback(row); @@ -230,13 +230,23 @@ const allKeys = (source, ...sources) => { // TODO: add check for consistent types for each key (dev only) return keys; }; -const recompute = (context, sources, target, path = []) => { - if (!path.length) { - if (!target) { - target = context.active; - } - if (!sources) { - sources = context.sources; +const recompute = (context, path = [], target, sources) => { + const setTargetAndSources = !target && !sources; + if (setTargetAndSources) { + ({ active: target, sources } = context); + if (path.length) { + for (let i = 0; i < path.length; i++) { + const seg = path[i]; + if (!target || !target[seg]) { + { + // eslint-disable-next-line no-console + console.error(`recompute: segment ${seg} not found on target`, path, target); + } + return; + } + target = target[seg]; + sources = sources.map(source => source[seg]).filter(Boolean); + } } } if (!target || !sources) { @@ -253,7 +263,15 @@ const recompute = (context, sources, target, path = []) => { for (const key of keys) { // This assumes consistent types usages for keys across sources // @ts-ignore - if (isPlainObject(sources[0][key])) { + let isObject = false; + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (source && key in source && source[key] !== undefined) { + isObject = isPlainObject(source[key]); + break; + } + } + if (isObject) { if (!target[key]) { target[key] = {}; } @@ -264,7 +282,7 @@ const recompute = (context, sources, target, path = []) => { keySources.push(source[key]); } } - recompute(context, keySources, target[key], [...path, key]); + recompute(context, [...path, key], target[key], keySources); continue; } // Ensure the target is an array if source is an array and target is empty @@ -278,7 +296,6 @@ const recompute = (context, sources, target, path = []) => { if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('RESOLVED', key, resolved, 'was', target[key]) target[key] = resolved; } }; @@ -309,6 +326,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (!isObject(value)) { return value; } + // Also return a merge proxy for nested objects if (!value[IS_PROXY]) { const keyPath = [...pathSegments, key]; value = createProxy(context, value, resolveContext, keyPath); @@ -346,6 +364,12 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ recompute(context); return success; } + else if (isPlainObject(value)) { + // if an object was assigned to this key make sure to recompute all + // of its individual properies + recompute(context, pathSegments); + return success; + } let keyContexts = []; let keySources; if (isArrayItem) { @@ -357,13 +381,13 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); // Ensure to clone if value is an object, cause sources is an array of - // the sourceProxies not the sources so we could trigger an endless loop when + // the sourceProxies and not the sources so we could trigger an endless loop when // updating a prop on an obj as the prop on the active object refers to // a prop on a proxy if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -376,7 +400,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ }, deleteProperty: (target, key) => { const success = Reflect.deleteProperty(target, key); - // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) if (success) { const isArrayItem = isArray(target); let activeSegmentKey; @@ -385,7 +409,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ let index = 0; for (const segment of pathSegments) { // @ts-ignore - proxies = proxies.map(proxy => proxy[segment]); + proxies = proxies.map(proxy => proxy && proxy[segment]); if (isArrayItem && index === pathSegments.length - 1) { activeSegmentKey = segment; break; @@ -395,7 +419,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } // Check if the key still exists in one of the sourceProxies, // if so resolve the new value, if not remove the key - if (proxies.some(proxy => (key in proxy))) { + if (proxies.some(proxy => proxy && (key in proxy))) { let keyContexts = []; let keySources; if (isArrayItem) { @@ -409,7 +433,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', resolved) + // console.log('SET VALUE', resolved) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -457,6 +481,7 @@ const createMergedObject = (resolve, active) => { }; }; +const cachedElements = {}; function renderMeta(context, key, data, config) { // console.info('renderMeta', key, data, config) if ('attributesFor' in config) { @@ -585,7 +610,7 @@ function renderTag(context, key, data, config = {}, groupConfig) { // console.info('FINAL TAG', finalTag) // console.log(' ATTRIBUTES', attributes) // console.log(' CONTENT', content) - // // console.log(data, attributes, config) + // console.log(data, attributes, config) if (isRaw && content) { attributes.innerHTML = content; } @@ -599,10 +624,10 @@ function renderTag(context, key, data, config = {}, groupConfig) { function renderAttributes(context, key, data, config) { // console.info('renderAttributes', key, data, config) const { attributesFor } = config; - if (!attributesFor) { + if (!attributesFor || !data) { return; } - { + if (context.isSSR) { // render attributes in a placeholder vnode so Vue // will render the string for us return { @@ -610,6 +635,37 @@ function renderAttributes(context, key, data, config) { vnode: vue.h(`ssr-${attributesFor}`, data) }; } + if (!cachedElements[attributesFor]) { + const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); + if (!el) { + // eslint-disable-next-line no-console + console.error('Could not find element for selector', attributesFor, ', won\'t render attributes'); + return; + } + if (el2) { + // eslint-disable-next-line no-console + console.warn('Found multiple elements for selector', attributesFor); + } + cachedElements[attributesFor] = { + el, + attrs: [] + }; + } + const { el, attrs } = cachedElements[attributesFor]; + for (const attr in data) { + let content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + if (isArray(content)) { + content = content.join(','); + } + 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); + } } function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { const slot = slots && slots[slotName]; @@ -658,7 +714,7 @@ function applyDifference(target, newSource, oldSource) { } } for (const key in oldSource) { - if (!(key in newSource)) { + if (!newSource || !(key in newSource)) { delete target[key]; } } @@ -711,9 +767,18 @@ const Metainfo = MetainfoImpl; const ssrAttribute = 'data-vm-ssr'; const active = vue.reactive({}); -function addVnode(teleports, to, vnodes) { +function addVnode(isSSR, teleports, to, vnodes) { const nodes = (isArray(vnodes) ? vnodes : [vnodes]); - if (!to.endsWith('Attrs')) { + if (!isSSR) { + // Comments shouldnt have any use on the client as they are not reactive anyway + nodes.forEach((vnode, idx) => { + if (vnode.type === vue.Comment) { + nodes.splice(idx, 1); + } + }); + // only add ssrAttribute's for real meta tags + } + else if (!to.endsWith('Attrs')) { nodes.forEach((vnode) => { if (!vnode.props) { vnode.props = {}; @@ -726,10 +791,12 @@ function addVnode(teleports, to, vnodes) { } teleports[to].push(...nodes); } -const createMetaManager = (config, resolver) => MetaManager.create(config || defaultConfig, resolver || defaultResolver); +const createMetaManager = (isSSR = false, config, resolver) => MetaManager.create(isSSR, config || defaultConfig, resolver || defaultResolver); class MetaManager { - constructor(config, target, resolver) { + constructor(isSSR, config, target, resolver) { + this.isSSR = false; this.ssrCleanedUp = false; + this.isSSR = isSSR; this.config = config; this.target = target; if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { @@ -749,8 +816,9 @@ class MetaManager { removed: [] }); const resolveContext = { vm }; - if (this.resolver) { - this.resolver.setup(resolveContext); + const { resolver } = this; + if (resolver && resolver.setup) { + resolver.setup(resolveContext); } // TODO: optimize initial compute (once) const meta = this.target.addSource(metadata, resolveContext, true); @@ -797,10 +865,25 @@ class MetaManager { } } render({ slots } = {}) { + // TODO: clean this method + const { isSSR } = this; + // cleanup ssr tags if not yet done + if (!isSSR && !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) { + ssrTags.forEach(el => el.parentNode && el.parentNode.removeChild(el)); + } + }, { once: true }); + } const teleports = {}; for (const key in active) { const config = this.config[key] || {}; - let renderedNodes = renderMeta({ metainfo: active, slots }, key, active[key], config); + let renderedNodes = renderMeta({ isSSR, metainfo: active, slots }, key, active[key], config); if (!renderedNodes) { continue; } @@ -815,7 +898,7 @@ class MetaManager { defaultTo = key; } for (const { to, vnode } of renderedNodes) { - addVnode(teleports, to || defaultTo || 'head', vnode); + addVnode(this.isSSR, teleports, to || defaultTo || 'head', vnode); } } if (slots) { @@ -827,16 +910,17 @@ class MetaManager { } const slot = slots[slotName]; if (isFunction(slot)) { - addVnode(teleports, tagName, slot({ metainfo: active })); + addVnode(this.isSSR, teleports, tagName, slot({ metainfo: active })); } } } return Object.keys(teleports).map((to) => { - return vue.h(vue.Teleport, { to }, teleports[to]); + const teleport = teleports[to]; + return vue.h(vue.Teleport, { to }, teleport); }); } } -MetaManager.create = (config, resolver) => { +MetaManager.create = (isSSR, config, resolver) => { const resolve = (options, contexts, active, key, pathSegments) => { if (isFunction(resolver)) { return resolver(options, contexts, active, key, pathSegments); @@ -845,7 +929,7 @@ MetaManager.create = (config, resolver) => { }; const mergedObject = createMergedObject(resolve, active); // TODO: validate resolver - const manager = new MetaManager(config, mergedObject, resolver); + const manager = new MetaManager(isSSR, config, mergedObject, resolver); return manager; }; diff --git a/dist/vue-meta.cjs.prod.js b/dist/vue-meta.cjs.prod.js index 2105c1c..11bd941 100644 --- a/dist/vue-meta.cjs.prod.js +++ b/dist/vue-meta.cjs.prod.js @@ -1,5 +1,5 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors @@ -27,7 +27,7 @@ const resolveOption = (predicament, initialValue) => (options, contexts) => { } }; -function setup(context) { +const setup = (context) => { let depth = 0; if (context.vm) { let { vm } = context; @@ -39,7 +39,7 @@ function setup(context) { } while (vm && vm.parent && vm !== vm.root); } context.depth = depth; -} +}; const resolve = resolveOption((currentValue, context) => { const { depth } = context; if (!currentValue || depth > currentValue) { @@ -200,7 +200,7 @@ function clone(v) { const pluck = (collection, key, callback) => { const plucked = []; for (const row of collection) { - if (key in row) { + if (row && key in row) { plucked.push(row[key]); if (callback) { callback(row); @@ -227,13 +227,19 @@ const allKeys = (source, ...sources) => { // TODO: add check for consistent types for each key (dev only) return keys; }; -const recompute = (context, sources, target, path = []) => { - if (!path.length) { - if (!target) { - target = context.active; - } - if (!sources) { - sources = context.sources; +const recompute = (context, path = [], target, sources) => { + const setTargetAndSources = !target && !sources; + if (setTargetAndSources) { + ({ active: target, sources } = context); + if (path.length) { + for (let i = 0; i < path.length; i++) { + const seg = path[i]; + if (!target || !target[seg]) { + return; + } + target = target[seg]; + sources = sources.map(source => source[seg]).filter(Boolean); + } } } if (!target || !sources) { @@ -250,7 +256,15 @@ const recompute = (context, sources, target, path = []) => { for (const key of keys) { // This assumes consistent types usages for keys across sources // @ts-ignore - if (isPlainObject(sources[0][key])) { + let isObject = false; + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (source && key in source && source[key] !== undefined) { + isObject = isPlainObject(source[key]); + break; + } + } + if (isObject) { if (!target[key]) { target[key] = {}; } @@ -261,7 +275,7 @@ const recompute = (context, sources, target, path = []) => { keySources.push(source[key]); } } - recompute(context, keySources, target[key], [...path, key]); + recompute(context, [...path, key], target[key], keySources); continue; } // Ensure the target is an array if source is an array and target is empty @@ -275,7 +289,6 @@ const recompute = (context, sources, target, path = []) => { if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('RESOLVED', key, resolved, 'was', target[key]) target[key] = resolved; } }; @@ -306,6 +319,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (!isObject(value)) { return value; } + // Also return a merge proxy for nested objects if (!value[IS_PROXY]) { const keyPath = [...pathSegments, key]; value = createProxy(context, value, resolveContext, keyPath); @@ -343,6 +357,12 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ recompute(context); return success; } + else if (isPlainObject(value)) { + // if an object was assigned to this key make sure to recompute all + // of its individual properies + recompute(context, pathSegments); + return success; + } let keyContexts = []; let keySources; if (isArrayItem) { @@ -354,13 +374,13 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); // Ensure to clone if value is an object, cause sources is an array of - // the sourceProxies not the sources so we could trigger an endless loop when + // the sourceProxies and not the sources so we could trigger an endless loop when // updating a prop on an obj as the prop on the active object refers to // a prop on a proxy if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -373,7 +393,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ }, deleteProperty: (target, key) => { const success = Reflect.deleteProperty(target, key); - // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) if (success) { const isArrayItem = isArray(target); let activeSegmentKey; @@ -382,7 +402,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ let index = 0; for (const segment of pathSegments) { // @ts-ignore - proxies = proxies.map(proxy => proxy[segment]); + proxies = proxies.map(proxy => proxy && proxy[segment]); if (isArrayItem && index === pathSegments.length - 1) { activeSegmentKey = segment; break; @@ -392,7 +412,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } // Check if the key still exists in one of the sourceProxies, // if so resolve the new value, if not remove the key - if (proxies.some(proxy => (key in proxy))) { + if (proxies.some(proxy => proxy && (key in proxy))) { let keyContexts = []; let keySources; if (isArrayItem) { @@ -406,7 +426,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', resolved) + // console.log('SET VALUE', resolved) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -454,6 +474,7 @@ const createMergedObject = (resolve, active) => { }; }; +const cachedElements = {}; function renderMeta(context, key, data, config) { // console.info('renderMeta', key, data, config) if ('attributesFor' in config) { @@ -578,7 +599,7 @@ function renderTag(context, key, data, config = {}, groupConfig) { // console.info('FINAL TAG', finalTag) // console.log(' ATTRIBUTES', attributes) // console.log(' CONTENT', content) - // // console.log(data, attributes, config) + // console.log(data, attributes, config) if (isRaw && content) { attributes.innerHTML = content; } @@ -592,10 +613,10 @@ function renderTag(context, key, data, config = {}, groupConfig) { function renderAttributes(context, key, data, config) { // console.info('renderAttributes', key, data, config) const { attributesFor } = config; - if (!attributesFor) { + if (!attributesFor || !data) { return; } - { + if (context.isSSR) { // render attributes in a placeholder vnode so Vue // will render the string for us return { @@ -603,6 +624,28 @@ function renderAttributes(context, key, data, config) { vnode: vue.h(`ssr-${attributesFor}`, data) }; } + if (!cachedElements[attributesFor]) { + const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); + cachedElements[attributesFor] = { + el, + attrs: [] + }; + } + const { el, attrs } = cachedElements[attributesFor]; + for (const attr in data) { + let content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + if (isArray(content)) { + content = content.join(','); + } + 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); + } } function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { const slot = slots && slots[slotName]; @@ -651,7 +694,7 @@ function applyDifference(target, newSource, oldSource) { } } for (const key in oldSource) { - if (!(key in newSource)) { + if (!newSource || !(key in newSource)) { delete target[key]; } } @@ -704,9 +747,18 @@ const Metainfo = MetainfoImpl; const ssrAttribute = 'data-vm-ssr'; const active = vue.reactive({}); -function addVnode(teleports, to, vnodes) { +function addVnode(isSSR, teleports, to, vnodes) { const nodes = (isArray(vnodes) ? vnodes : [vnodes]); - if (!to.endsWith('Attrs')) { + if (!isSSR) { + // Comments shouldnt have any use on the client as they are not reactive anyway + nodes.forEach((vnode, idx) => { + if (vnode.type === vue.Comment) { + nodes.splice(idx, 1); + } + }); + // only add ssrAttribute's for real meta tags + } + else if (!to.endsWith('Attrs')) { nodes.forEach((vnode) => { if (!vnode.props) { vnode.props = {}; @@ -719,10 +771,12 @@ function addVnode(teleports, to, vnodes) { } teleports[to].push(...nodes); } -const createMetaManager = (config, resolver) => MetaManager.create(config || defaultConfig, resolver || defaultResolver); +const createMetaManager = (isSSR = false, config, resolver) => MetaManager.create(isSSR, config || defaultConfig, resolver || defaultResolver); class MetaManager { - constructor(config, target, resolver) { + constructor(isSSR, config, target, resolver) { + this.isSSR = false; this.ssrCleanedUp = false; + this.isSSR = isSSR; this.config = config; this.target = target; if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { @@ -742,8 +796,9 @@ class MetaManager { removed: [] }); const resolveContext = { vm }; - if (this.resolver) { - this.resolver.setup(resolveContext); + const { resolver } = this; + if (resolver && resolver.setup) { + resolver.setup(resolveContext); } // TODO: optimize initial compute (once) const meta = this.target.addSource(metadata, resolveContext, true); @@ -790,10 +845,25 @@ class MetaManager { } } render({ slots } = {}) { + // TODO: clean this method + const { isSSR } = this; + // cleanup ssr tags if not yet done + if (!isSSR && !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) { + ssrTags.forEach(el => el.parentNode && el.parentNode.removeChild(el)); + } + }, { once: true }); + } const teleports = {}; for (const key in active) { const config = this.config[key] || {}; - let renderedNodes = renderMeta({ metainfo: active, slots }, key, active[key], config); + let renderedNodes = renderMeta({ isSSR, metainfo: active, slots }, key, active[key], config); if (!renderedNodes) { continue; } @@ -808,7 +878,7 @@ class MetaManager { defaultTo = key; } for (const { to, vnode } of renderedNodes) { - addVnode(teleports, to || defaultTo || 'head', vnode); + addVnode(this.isSSR, teleports, to || defaultTo || 'head', vnode); } } if (slots) { @@ -820,16 +890,17 @@ class MetaManager { } const slot = slots[slotName]; if (isFunction(slot)) { - addVnode(teleports, tagName, slot({ metainfo: active })); + addVnode(this.isSSR, teleports, tagName, slot({ metainfo: active })); } } } return Object.keys(teleports).map((to) => { - return vue.h(vue.Teleport, { to }, teleports[to]); + const teleport = teleports[to]; + return vue.h(vue.Teleport, { to }, teleport); }); } } -MetaManager.create = (config, resolver) => { +MetaManager.create = (isSSR, config, resolver) => { const resolve = (options, contexts, active, key, pathSegments) => { if (isFunction(resolver)) { return resolver(options, contexts, active, key, pathSegments); @@ -838,7 +909,7 @@ MetaManager.create = (config, resolver) => { }; const mergedObject = createMergedObject(resolve, active); // TODO: validate resolver - const manager = new MetaManager(config, mergedObject, resolver); + const manager = new MetaManager(isSSR, config, mergedObject, resolver); return manager; }; diff --git a/dist/vue-meta.d.ts b/dist/vue-meta.d.ts index 490609b..2874f49 100644 --- a/dist/vue-meta.d.ts +++ b/dist/vue-meta.d.ts @@ -1,3 +1,13 @@ +/** + * vue-meta v3.0.0-alpha.6 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ + +/// + import { App, ComponentInternalInstance, Slots, VNode } from 'vue'; declare const IS_PROXY: unique symbol; @@ -35,15 +45,16 @@ declare type MergedObjectBuilder = { delSource: (sourceOrProxy: T | MergeSource, recompute?: boolean) => boolean; }; -declare type createMetaManagerMethod = (config: MetaConfig, resolver: MetaResolver | ResolveMethod) => MetaManager; -declare const createMetaManager: (config?: MetaConfig | undefined, resolver?: MetaResolver | undefined) => MetaManager; +declare type CreateMetaManagerMethod = (isSSR: boolean, config: MetaConfig, resolver: MetaResolver | ResolveMethod) => MetaManager; +declare const createMetaManager: (isSSR?: boolean, config?: MetaConfig | undefined, resolver?: MetaResolver | undefined) => MetaManager; declare class MetaManager { + isSSR: boolean; config: MetaConfig; target: MergedObjectBuilder; - resolver?: MetaResolverSetup; + resolver?: MetaResolver; ssrCleanedUp: boolean; - constructor(config: MetaConfig, target: MergedObjectBuilder, resolver: MetaResolver | ResolveMethod); - static create: createMetaManagerMethod; + constructor(isSSR: boolean, config: MetaConfig, target: MergedObjectBuilder, resolver: MetaResolver | ResolveMethod); + static create: CreateMetaManagerMethod; install(app: App): void; addMeta(metadata: MetaSource, vm?: ComponentInternalInstance): MetaProxy; private unmount; @@ -116,16 +127,14 @@ interface MetaActive { * Context passed to the meta resolvers */ declare type MetaResolveContext = ResolveContext & { - vm: ComponentInternalInstance | undefined; + vm?: ComponentInternalInstance; }; declare type MetaResolveSetup = (context: MetaResolveContext) => void; declare type MetaResolver = { setup?: MetaResolveSetup; resolve: ResolveMethod; }; -declare type MetaResolverSetup = Modify; +declare type MetaResolverSetup = Required; /** * @internal */ @@ -142,8 +151,9 @@ interface MetaGuards { * @internal */ interface MetaRenderContext { - slots?: Slots; + isSSR: boolean; metainfo: MetaActive; + slots?: Slots; } /** * @internal @@ -181,11 +191,8 @@ declare module '@vue/runtime-core' { } } -declare type MergeResolveContextDeepest = MetaResolveContext & { - depth: number; -}; -declare function setup(context: MergeResolveContextDeepest): void; -declare const resolve: ResolveMethod; +declare const setup: MetaResolveSetup; +declare const resolve: ResolveMethod; declare const deepest_d_setup: typeof setup; declare const deepest_d_resolve: typeof resolve; diff --git a/dist/vue-meta.esm-browser.js b/dist/vue-meta.esm-browser.js index 2d611e8..0b9b23e 100644 --- a/dist/vue-meta.esm-browser.js +++ b/dist/vue-meta.esm-browser.js @@ -1,5 +1,5 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors @@ -23,7 +23,7 @@ const resolveOption = (predicament, initialValue) => (options, contexts) => { } }; -function setup(context) { +const setup = (context) => { let depth = 0; if (context.vm) { let { vm } = context; @@ -35,7 +35,7 @@ function setup(context) { } while (vm && vm.parent && vm !== vm.root); } context.depth = depth; -} +}; const resolve = resolveOption((currentValue, context) => { const { depth } = context; if (!currentValue || depth > currentValue) { @@ -199,7 +199,7 @@ function clone(v) { const pluck = (collection, key, callback) => { const plucked = []; for (const row of collection) { - if (key in row) { + if (row && key in row) { plucked.push(row[key]); if (callback) { callback(row); @@ -226,13 +226,23 @@ const allKeys = (source, ...sources) => { // TODO: add check for consistent types for each key (dev only) return keys; }; -const recompute = (context, sources, target, path = []) => { - if (!path.length) { - if (!target) { - target = context.active; - } - if (!sources) { - sources = context.sources; +const recompute = (context, path = [], target, sources) => { + const setTargetAndSources = !target && !sources; + if (setTargetAndSources) { + ({ active: target, sources } = context); + if (path.length) { + for (let i = 0; i < path.length; i++) { + const seg = path[i]; + if (!target || !target[seg]) { + { + // eslint-disable-next-line no-console + console.error(`recompute: segment ${seg} not found on target`, path, target); + } + return; + } + target = target[seg]; + sources = sources.map(source => source[seg]).filter(Boolean); + } } } if (!target || !sources) { @@ -249,7 +259,15 @@ const recompute = (context, sources, target, path = []) => { for (const key of keys) { // This assumes consistent types usages for keys across sources // @ts-ignore - if (isPlainObject(sources[0][key])) { + let isObject = false; + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (source && key in source && source[key] !== undefined) { + isObject = isPlainObject(source[key]); + break; + } + } + if (isObject) { if (!target[key]) { target[key] = {}; } @@ -260,7 +278,7 @@ const recompute = (context, sources, target, path = []) => { keySources.push(source[key]); } } - recompute(context, keySources, target[key], [...path, key]); + recompute(context, [...path, key], target[key], keySources); continue; } // Ensure the target is an array if source is an array and target is empty @@ -274,7 +292,6 @@ const recompute = (context, sources, target, path = []) => { if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('RESOLVED', key, resolved, 'was', target[key]) target[key] = resolved; } }; @@ -305,6 +322,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (!isObject(value)) { return value; } + // Also return a merge proxy for nested objects if (!value[IS_PROXY]) { const keyPath = [...pathSegments, key]; value = createProxy(context, value, resolveContext, keyPath); @@ -342,6 +360,12 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ recompute(context); return success; } + else if (isPlainObject(value)) { + // if an object was assigned to this key make sure to recompute all + // of its individual properies + recompute(context, pathSegments); + return success; + } let keyContexts = []; let keySources; if (isArrayItem) { @@ -353,13 +377,13 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); // Ensure to clone if value is an object, cause sources is an array of - // the sourceProxies not the sources so we could trigger an endless loop when + // the sourceProxies and not the sources so we could trigger an endless loop when // updating a prop on an obj as the prop on the active object refers to // a prop on a proxy if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -372,7 +396,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ }, deleteProperty: (target, key) => { const success = Reflect.deleteProperty(target, key); - // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) if (success) { const isArrayItem = isArray(target); let activeSegmentKey; @@ -381,7 +405,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ let index = 0; for (const segment of pathSegments) { // @ts-ignore - proxies = proxies.map(proxy => proxy[segment]); + proxies = proxies.map(proxy => proxy && proxy[segment]); if (isArrayItem && index === pathSegments.length - 1) { activeSegmentKey = segment; break; @@ -391,7 +415,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } // Check if the key still exists in one of the sourceProxies, // if so resolve the new value, if not remove the key - if (proxies.some(proxy => (key in proxy))) { + if (proxies.some(proxy => proxy && (key in proxy))) { let keyContexts = []; let keySources; if (isArrayItem) { @@ -405,7 +429,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', resolved) + // console.log('SET VALUE', resolved) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -582,7 +606,7 @@ function renderTag(context, key, data, config = {}, groupConfig) { // console.info('FINAL TAG', finalTag) // console.log(' ATTRIBUTES', attributes) // console.log(' CONTENT', content) - // // console.log(data, attributes, config) + // console.log(data, attributes, config) if (isRaw && content) { attributes.innerHTML = content; } @@ -596,9 +620,17 @@ function renderTag(context, key, data, config = {}, groupConfig) { function renderAttributes(context, key, data, config) { // console.info('renderAttributes', key, data, config) const { attributesFor } = config; - if (!attributesFor) { + if (!attributesFor || !data) { return; } + if (context.isSSR) { + // render attributes in a placeholder vnode so Vue + // will render the string for us + return { + to: '', + vnode: h(`ssr-${attributesFor}`, data) + }; + } if (!cachedElements[attributesFor]) { const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); if (!el) { @@ -617,7 +649,10 @@ function renderAttributes(context, key, data, config) { } const { el, attrs } = cachedElements[attributesFor]; for (const attr in data) { - const content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + let content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + if (isArray(content)) { + content = content.join(','); + } el.setAttribute(attr, content || ''); if (!attrs.includes(attr)) { attrs.push(attr); @@ -675,7 +710,7 @@ function applyDifference(target, newSource, oldSource) { } } for (const key in oldSource) { - if (!(key in newSource)) { + if (!newSource || !(key in newSource)) { delete target[key]; } } @@ -728,9 +763,9 @@ const Metainfo = MetainfoImpl; const ssrAttribute = 'data-vm-ssr'; const active = reactive({}); -function addVnode(teleports, to, vnodes) { +function addVnode(isSSR, teleports, to, vnodes) { const nodes = (isArray(vnodes) ? vnodes : [vnodes]); - { + if (!isSSR) { // Comments shouldnt have any use on the client as they are not reactive anyway nodes.forEach((vnode, idx) => { if (vnode.type === Comment) { @@ -739,15 +774,25 @@ function addVnode(teleports, to, vnodes) { }); // only add ssrAttribute's for real meta tags } + else if (!to.endsWith('Attrs')) { + nodes.forEach((vnode) => { + if (!vnode.props) { + vnode.props = {}; + } + vnode.props[ssrAttribute] = true; + }); + } if (!teleports[to]) { teleports[to] = []; } teleports[to].push(...nodes); } -const createMetaManager = (config, resolver) => MetaManager.create(config || defaultConfig, resolver || defaultResolver); +const createMetaManager = (isSSR = false, config, resolver) => MetaManager.create(isSSR, config || defaultConfig, resolver || defaultResolver); class MetaManager { - constructor(config, target, resolver) { + constructor(isSSR, config, target, resolver) { + this.isSSR = false; this.ssrCleanedUp = false; + this.isSSR = isSSR; this.config = config; this.target = target; if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { @@ -767,8 +812,9 @@ class MetaManager { removed: [] }); const resolveContext = { vm }; - if (this.resolver) { - this.resolver.setup(resolveContext); + const { resolver } = this; + if (resolver && resolver.setup) { + resolver.setup(resolveContext); } // TODO: optimize initial compute (once) const meta = this.target.addSource(metadata, resolveContext, true); @@ -816,8 +862,9 @@ class MetaManager { } render({ slots } = {}) { // TODO: clean this method + const { isSSR } = this; // cleanup ssr tags if not yet done - if (!this.ssrCleanedUp) { + if (!isSSR && !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 @@ -825,14 +872,14 @@ class MetaManager { window.addEventListener('DOMContentLoaded', () => { const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`); if (ssrTags && ssrTags.length) { - Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el)); + ssrTags.forEach(el => el.parentNode && el.parentNode.removeChild(el)); } - }); + }, { once: true }); } const teleports = {}; for (const key in active) { const config = this.config[key] || {}; - let renderedNodes = renderMeta({ metainfo: active, slots }, key, active[key], config); + let renderedNodes = renderMeta({ isSSR, metainfo: active, slots }, key, active[key], config); if (!renderedNodes) { continue; } @@ -847,7 +894,7 @@ class MetaManager { defaultTo = key; } for (const { to, vnode } of renderedNodes) { - addVnode(teleports, to || defaultTo || 'head', vnode); + addVnode(this.isSSR, teleports, to || defaultTo || 'head', vnode); } } if (slots) { @@ -859,16 +906,17 @@ class MetaManager { } const slot = slots[slotName]; if (isFunction(slot)) { - addVnode(teleports, tagName, slot({ metainfo: active })); + addVnode(this.isSSR, teleports, tagName, slot({ metainfo: active })); } } } return Object.keys(teleports).map((to) => { - return h(Teleport, { to }, teleports[to]); + const teleport = teleports[to]; + return h(Teleport, { to }, teleport); }); } } -MetaManager.create = (config, resolver) => { +MetaManager.create = (isSSR, config, resolver) => { const resolve = (options, contexts, active, key, pathSegments) => { if (isFunction(resolver)) { return resolver(options, contexts, active, key, pathSegments); @@ -877,7 +925,7 @@ MetaManager.create = (config, resolver) => { }; const mergedObject = createMergedObject(resolve, active); // TODO: validate resolver - const manager = new MetaManager(config, mergedObject, resolver); + const manager = new MetaManager(isSSR, config, mergedObject, resolver); return manager; }; diff --git a/dist/vue-meta.esm-browser.min.js b/dist/vue-meta.esm-browser.min.js index 2539979..1502d9c 100644 --- a/dist/vue-meta.esm-browser.min.js +++ b/dist/vue-meta.esm-browser.min.js @@ -1,8 +1,8 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors * @license MIT */ -import{markRaw as t,h as e,getCurrentInstance as o,isProxy as r,watch as n,inject as s,defineComponent as i,reactive as c,onUnmounted as a,Teleport as u,Comment as l}from"vue";const f=(t,e)=>(o,r)=>{let n=-1;if(r.reduce(((e,o,r)=>{const s=t(e,o);return s!==e?(n=r,s):e}),e),n>-1)return o[n]};const d=f(((t,e)=>{const{depth:o}=e;return!t||o>t?o:t}));var p=Object.freeze({__proto__:null,setup:function(t){let e=0;if(t.vm){let{vm:o}=t;do{o.parent&&(e++,o=o.parent)}while(o&&o.parent&&o!==o.root)}t.depth=e},resolve:d});const m={body:{tag:"script",to:"body"},base:{valueAttribute:"href"},charset:{tag:"meta",nameless:!0,valueAttribute:"charset"},description:{tag:"meta"},og:{group:!0,namespacedAttribute:!0,tag:"meta",keyAttribute:"property"},twitter:{group:!0,namespacedAttribute:!0,tag:"meta"},htmlAttrs:{attributesFor:"html"},headAttrs:{attributesFor:"head"},bodyAttrs:{attributesFor:"body"}},b={title:{attributes:!1},base:{contentAsAttribute:!0,attributes:["href","target"]},meta:{contentAsAttribute:!0,keyAttribute:"name",attributes:["content","name","http-equiv","charset"]},link:{contentAsAttribute:!0,attributes:["href","crossorigin","rel","media","integrity","hreflang","type","referrerpolicy","sizes","imagesrcset","imagesizes","as","color"]},style:{attributes:["media"]},script:{attributes:["src","type","nomodule","async","defer","crossorigin","integrity","referrerpolicy"]},noscript:{attributes:!1}};Object.freeze({}),Object.freeze([]);const h=Array.isArray,y=t=>"function"==typeof t,g=t=>"string"==typeof t,v=t=>null!==t&&"object"==typeof t,A=Object.prototype.toString,S=t=>"[object Object]"===A.call(t),k=Symbol("kIsProxy"),w=Symbol("kProxySources"),j=Symbol("kProxyTarget"),N=Symbol("kResolveContext");function O(t){if(h(t))return t.map(O);if(v(t)){const e={};for(const o in t)e[o]="context"===o?t[o]:O(t[o]);return e}return t}const x=(t,e,o)=>{const r=[];for(const n of t)e in n&&(r.push(n[e]),o&&o(n));return r},$=(t,e,o,r=[])=>{if(r.length||(o||(o=t.active),e||(e=t.sources)),!o||!e)return;const n=((t,...e)=>{const o=t?Object.keys(t):[];if(e)for(const t of e)if(t&&v(t))for(const e in t)o.includes(e)||o.push(e);return o})(...e),s=Object.keys(o);for(const t of s)n.includes(t)||delete o[t];for(const s of n){if(S(e[0][s])){o[s]||(o[s]={});const n=[];for(const t of e)s in t&&n.push(t[s]);$(t,n,o[s],[...r,s]);continue}!o[s]&&h(e[0][s])&&(o[s]=[]);const n=[],i=x(e,s,(t=>n.push(t[N])));let c=t.resolve(i,n,o[s],s,r);S(c)&&(c=O(c)),o[s]=c}},P=(e,o,r,n=[])=>{const s=C(e,r,n),i=t(new Proxy(o,s));return!n.length&&e.sources&&e.sources.push(i),i},C=(t,e,o=[])=>({get:(r,n,s)=>{if(n===k)return!0;if(n===w)return t.sources;if(n===j)return r;if(n===N)return e;let i=Reflect.get(r,n,s);if(!v(i))return i;if(!i[k]){const s=[...o,n];i=P(t,i,e,s),Reflect.set(r,n,i)}return i},set:(e,r,n)=>{const s=Reflect.set(e,r,n);if(s){const n=h(e);let i,c=!1,{sources:a,active:u}=t,l=0;for(const t of o){if(a=x(a,t),n&&l===o.length-1){i=t;break}h(u)&&(c=!0),u=u[t],l++}if(c)return $(t),s;let f,d=[];n?(f=a,d=a.map((t=>t[N]))):f=x(a,r,(t=>d.push(t[N])));let p=t.resolve(f,d,u,r,o);S(p)&&(p=O(p)),n&&i?u[i]=p:u[r]=p}return s},deleteProperty:(e,r)=>{const n=Reflect.deleteProperty(e,r);if(n){const n=h(e);let s,i=t.sources,c=t.active,a=0;for(const t of o){if(i=i.map((e=>e[t])),n&&a===o.length-1){s=t;break}c=c[t],a++}if(i.some((t=>r in t))){let e,a=[];n?(e=i,a=i.map((t=>t[N]))):e=x(i,r,(t=>a.push(t[N])));let u=t.resolve(e,a,c,r,o);S(u)&&(u=O(u)),n&&s?c[s]=u:c[r]=u}else delete c[r]}return n}}),M={};function F(t,e,o,r){return"attributesFor"in r?function(t,e,o,r){const{attributesFor:n}=r;if(!n)return;if(!M[n]){const[t,e]=Array.from(document.querySelectorAll(n));if(!t)return void console.error("Could not find element for selector",n,", won't render attributes");e&&console.warn("Found multiple elements for selector",n),M[n]={el:t,attrs:[]}}const{el:s,attrs:i}=M[n];for(const r in o){const n=U(t,`${e}(${r})`,o[r],o);s.setAttribute(r,n||""),i.includes(r)||i.push(r)}const c=i.filter((t=>!o[t]));for(const t of c)s.removeAttribute(t)}(t,e,o,r):"group"in r?function(t,e,o,r){if(h(o))return console.warn("Specifying an array for group properties isnt supported"),[];return Object.keys(o).map((n=>{const s={group:e,data:o};if(r.namespaced)s.tagNamespace=!0===r.namespaced?e:r.namespaced;else if(r.namespacedAttribute){const t=!0===r.namespacedAttribute?e:r.namespacedAttribute;s.fullName=`${t}:${n}`,s.slotName=`${t}(${n})`}return R(t,e,o[n],r,s)})).flat()}(t,e,o,r):R(t,e,o,r)}function R(t,o,r,n={},s){const i=["content","json","rawContent"],c=t=>function(t,e){for(const o of t){const t=b[o];if(o&&t)return t[e]}}([a,n.tag],t);if(h(r))return r.map((e=>R(t,o,e,n,s))).flat();const{tag:a=n.tag||o}=r;let u="",l=!1,f=!1;if(g(r))u=r;else if(r.children&&h(r.children))l=!0,u=r.children.map((e=>{const r=R(t,o,e,n,s);return h(r)?r.map((({vnode:t})=>t)):r.vnode}));else{let t=0;for(const e of i){if(!u&&r[e]){u=1===t?JSON.stringify(r[e]):r[e],f=t>1;break}t++}}const d=s&&s.fullName||o,p=s&&s.slotName||o;let{attrs:m}=r;if(m||"object"!=typeof r)m||(m={});else{m={...r},delete m.tag,delete m.children,delete m.to;for(const t of i)delete m[t]}if(l)u=U(t,p,u,r);else{const e=!!c("contentAsAttribute");let{valueAttribute:o}=n;if(!o&&e){const[t]=c("attributes");o=g(e)?e:t}if(o){const{nameless:e,keyAttribute:r}=n;e||r&&(m[r]=d),m[o]=U(t,p,m[o]||u,s),u=""}else u=U(t,p,u,r)}const y=s&&s.tagNamespace?`${s.tagNamespace}:${a}`:a;f&&u&&(m.innerHTML=u);const v=e(y,m,u||void 0);return{to:r.to,vnode:v}}function U({metainfo:t,slots:e},o,r,n){const s=e&&e[o];if(!s||!y(s))return r;const i={content:r,metainfo:t};if(n&&n.group){const{group:t,data:e}=n;i[t]=e}const c=s(i);if(c&&c.length){const{children:t}=c[0];return t?t.toString():""}return r}const z="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,E=(t=>z?Symbol("[vue-meta]: "+t):"[vue-meta]: "+t)("meta_active");function _(t,e,o){for(const r in e)r in o?v(t[r])?_(t[r],e[r],o[r]):e[r]!==o[r]&&(t[r]=e[r]):t[r]=e[r];for(const r in o)r in e||delete t[r]}function L(t){if(t||(t=o()||void 0),t)return t.appContext.config.globalProperties.$metaManager}function q(t,e){const s=o()||void 0;if(!e&&s&&(e=L(s)),!e)throw new Error("No manager or current instance");r(t)&&(n(t,((t,e)=>{_(i.meta,t,e)})),t=t.value);const i=e.addMeta(t,s);return i}function T(){return s(E)}const I=i({name:"Metainfo",inheritAttrs:!1,setup:(t,{slots:e})=>()=>{const t=L();if(t)return t.render({slots:e})}}),D=c({});function H(t,e,o){const r=h(o)?o:[o];r.forEach(((t,e)=>{t.type===l&&r.splice(e,1)})),t[e]||(t[e]=[]),t[e].push(...r)}const J=(t,e)=>B.create(t||m,e||p);class B{constructor(t,e,o){this.ssrCleanedUp=!1,this.config=t,this.target=e,o&&"setup"in o&&y(o.setup)&&(this.resolver=o)}install(t){t.component("Metainfo",I),t.config.globalProperties.$metaManager=this,t.provide(E,D)}addMeta(t,e){e||(e=o()||void 0);const r={removed:[]},n={vm:e};this.resolver&&this.resolver.setup(n);const s=this.target.addSource(t,n,!0),i=t=>this.unmount(!!t,s,r,e);return e&&a(i),{meta:s,onRemoved:t=>r.removed.push(t),unmount:i}}unmount(t,e,o,r){if(r){const{$el:n}=r.proxy;if(n&&n.offsetParent){let r=new MutationObserver((s=>{for(const{removedNodes:i}of s)i&&i.forEach((s=>{s===n&&r&&(r.disconnect(),r=void 0,this.reallyUnmount(t,e,o))}))}));return void r.observe(n.parentNode,{childList:!0})}}this.reallyUnmount(t,e,o)}async reallyUnmount(t,e,o){this.target.delSource(e),!t&&o&&await Promise.all(o.removed.map((t=>t())))}render({slots:t}={}){this.ssrCleanedUp||(this.ssrCleanedUp=!0,window.addEventListener("DOMContentLoaded",(()=>{const t=document.querySelectorAll("[data-vm-ssr]");t&&t.length&&Array.from(t).forEach((t=>t.parentNode&&t.parentNode.removeChild(t)))})));const o={};for(const e in D){const r=this.config[e]||{};let n=F({metainfo:D,slots:t},e,D[e],r);if(!n)continue;h(n)||(n=[n]);let s="base"!==e&&D[e].to;!s&&"to"in r&&(s=r.to),!s&&"attributesFor"in r&&(s=e);for(const{to:t,vnode:e}of n)H(o,t||s||"head",e)}if(t)for(const e in t){const r="default"===e?"head":e;if("head"!==r&&"body"!==r)continue;const n=t[e];y(n)&&H(o,r,n({metainfo:D}))}return Object.keys(o).map((t=>e(u,{to:t},o[t])))}}B.create=(t,e)=>{const o=((t,e)=>{const o=[],r={active:e,resolve:t,sources:o},n=()=>$(r);return{context:r,compute:n,addSource:(t,e,o=!1)=>{const s=P(r,t,e||{});return o&&n(),s},delSource:(t,e=!0)=>{const r=o.findIndex((e=>e===t||e[j]===t));return r>-1&&(o.splice(r,1),e&&n(),!0)}}})(((t,o,r,n,s)=>y(e)?e(t,o,r,n,s):e.resolve(t,o,r,n,s)),D);return new B(t,o,e)};export{J as createMetaManager,p as deepestResolver,m as defaultConfig,L as getCurrentManager,f as resolveOption,T as useActiveMeta,q as useMeta}; +import{markRaw as t,h as e,getCurrentInstance as o,isProxy as r,watch as n,inject as s,defineComponent as i,reactive as c,onUnmounted as a,Teleport as u,Comment as l}from"vue";const f=(t,e)=>(o,r)=>{let n=-1;if(r.reduce(((e,o,r)=>{const s=t(e,o);return s!==e?(n=r,s):e}),e),n>-1)return o[n]},d=f(((t,e)=>{const{depth:o}=e;return!t||o>t?o:t}));var p=Object.freeze({__proto__:null,setup:t=>{let e=0;if(t.vm){let{vm:o}=t;do{o.parent&&(e++,o=o.parent)}while(o&&o.parent&&o!==o.root)}t.depth=e},resolve:d});const m={body:{tag:"script",to:"body"},base:{valueAttribute:"href"},charset:{tag:"meta",nameless:!0,valueAttribute:"charset"},description:{tag:"meta"},og:{group:!0,namespacedAttribute:!0,tag:"meta",keyAttribute:"property"},twitter:{group:!0,namespacedAttribute:!0,tag:"meta"},htmlAttrs:{attributesFor:"html"},headAttrs:{attributesFor:"head"},bodyAttrs:{attributesFor:"body"}},h={title:{attributes:!1},base:{contentAsAttribute:!0,attributes:["href","target"]},meta:{contentAsAttribute:!0,keyAttribute:"name",attributes:["content","name","http-equiv","charset"]},link:{contentAsAttribute:!0,attributes:["href","crossorigin","rel","media","integrity","hreflang","type","referrerpolicy","sizes","imagesrcset","imagesizes","as","color"]},style:{attributes:["media"]},script:{attributes:["src","type","nomodule","async","defer","crossorigin","integrity","referrerpolicy"]},noscript:{attributes:!1}};Object.freeze({}),Object.freeze([]);const b=Array.isArray,g=t=>"function"==typeof t,y=t=>"string"==typeof t,v=t=>null!==t&&"object"==typeof t,S=Object.prototype.toString,A=t=>"[object Object]"===S.call(t),k=Symbol("kIsProxy"),j=Symbol("kProxySources"),w=Symbol("kProxyTarget"),R=Symbol("kResolveContext");function N(t){if(b(t))return t.map(N);if(v(t)){const e={};for(const o in t)e[o]="context"===o?t[o]:N(t[o]);return e}return t}const $=(t,e,o)=>{const r=[];for(const n of t)n&&e in n&&(r.push(n[e]),o&&o(n));return r},O=(t,e=[],o,r)=>{if(!o&&!r&&(({active:o,sources:r}=t),e.length))for(let t=0;tt[n])).filter(Boolean)}if(!o||!r)return;const n=((t,...e)=>{const o=t?Object.keys(t):[];if(e)for(const t of e)if(t&&v(t))for(const e in t)o.includes(e)||o.push(e);return o})(...r),s=Object.keys(o);for(const t of s)n.includes(t)||delete o[t];for(const s of n){let n=!1;for(let t=0;ti.push(t[R])));let a=t.resolve(c,i,o[s],s,e);A(a)&&(a=N(a)),o[s]=a}},x=(e,o,r,n=[])=>{const s=P(e,r,n),i=t(new Proxy(o,s));return!n.length&&e.sources&&e.sources.push(i),i},P=(t,e,o=[])=>({get:(r,n,s)=>{if(n===k)return!0;if(n===j)return t.sources;if(n===w)return r;if(n===R)return e;let i=Reflect.get(r,n,s);if(!v(i))return i;if(!i[k]){const s=[...o,n];i=x(t,i,e,s),Reflect.set(r,n,i)}return i},set:(e,r,n)=>{const s=Reflect.set(e,r,n);if(s){const i=b(e);let c,a=!1,{sources:u,active:l}=t,f=0;for(const t of o){if(u=$(u,t),i&&f===o.length-1){c=t;break}b(l)&&(a=!0),l=l[t],f++}if(a)return O(t),s;if(A(n))return O(t,o),s;let d,p=[];i?(d=u,p=u.map((t=>t[R]))):d=$(u,r,(t=>p.push(t[R])));let m=t.resolve(d,p,l,r,o);A(m)&&(m=N(m)),i&&c?l[c]=m:l[r]=m}return s},deleteProperty:(e,r)=>{const n=Reflect.deleteProperty(e,r);if(n){const n=b(e);let s,i=t.sources,c=t.active,a=0;for(const t of o){if(i=i.map((e=>e&&e[t])),n&&a===o.length-1){s=t;break}c=c[t],a++}if(i.some((t=>t&&r in t))){let e,a=[];n?(e=i,a=i.map((t=>t[R]))):e=$(i,r,(t=>a.push(t[R])));let u=t.resolve(e,a,c,r,o);A(u)&&(u=N(u)),n&&s?c[s]=u:c[r]=u}else delete c[r]}return n}}),C={};function M(t,o,r,n){return"attributesFor"in n?function(t,o,r,n){const{attributesFor:s}=n;if(!s||!r)return;if(t.isSSR)return{to:"",vnode:e(`ssr-${s}`,r)};if(!C[s]){const[t,e]=Array.from(document.querySelectorAll(s));if(!t)return void console.error("Could not find element for selector",s,", won't render attributes");e&&console.warn("Found multiple elements for selector",s),C[s]={el:t,attrs:[]}}const{el:i,attrs:c}=C[s];for(const e in r){let n=E(t,`${o}(${e})`,r[e],r);b(n)&&(n=n.join(",")),i.setAttribute(e,n||""),c.includes(e)||c.push(e)}const a=c.filter((t=>!r[t]));for(const t of a)i.removeAttribute(t)}(t,o,r,n):"group"in n?function(t,e,o,r){if(b(o))return console.warn("Specifying an array for group properties isnt supported"),[];return Object.keys(o).map((n=>{const s={group:e,data:o};if(r.namespaced)s.tagNamespace=!0===r.namespaced?e:r.namespaced;else if(r.namespacedAttribute){const t=!0===r.namespacedAttribute?e:r.namespacedAttribute;s.fullName=`${t}:${n}`,s.slotName=`${t}(${n})`}return F(t,e,o[n],r,s)})).flat()}(t,o,r,n):F(t,o,r,n)}function F(t,o,r,n={},s){const i=["content","json","rawContent"],c=t=>function(t,e){for(const o of t){const t=h[o];if(o&&t)return t[e]}}([a,n.tag],t);if(b(r))return r.map((e=>F(t,o,e,n,s))).flat();const{tag:a=n.tag||o}=r;let u="",l=!1,f=!1;if(y(r))u=r;else if(r.children&&b(r.children))l=!0,u=r.children.map((e=>{const r=F(t,o,e,n,s);return b(r)?r.map((({vnode:t})=>t)):r.vnode}));else{let t=0;for(const e of i){if(!u&&r[e]){u=1===t?JSON.stringify(r[e]):r[e],f=t>1;break}t++}}const d=s&&s.fullName||o,p=s&&s.slotName||o;let{attrs:m}=r;if(m||"object"!=typeof r)m||(m={});else{m={...r},delete m.tag,delete m.children,delete m.to;for(const t of i)delete m[t]}if(l)u=E(t,p,u,r);else{const e=!!c("contentAsAttribute");let{valueAttribute:o}=n;if(!o&&e){const[t]=c("attributes");o=y(e)?e:t}if(o){const{nameless:e,keyAttribute:r}=n;e||r&&(m[r]=d),m[o]=E(t,p,m[o]||u,s),u=""}else u=E(t,p,u,r)}const g=s&&s.tagNamespace?`${s.tagNamespace}:${a}`:a;f&&u&&(m.innerHTML=u);const v=e(g,m,u||void 0);return{to:r.to,vnode:v}}function E({metainfo:t,slots:e},o,r,n){const s=e&&e[o];if(!s||!g(s))return r;const i={content:r,metainfo:t};if(n&&n.group){const{group:t,data:e}=n;i[t]=e}const c=s(i);if(c&&c.length){const{children:t}=c[0];return t?t.toString():""}return r}const U="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,z=(t=>U?Symbol("[vue-meta]: "+t):"[vue-meta]: "+t)("meta_active");function _(t,e,o){for(const r in e)r in o?v(t[r])?_(t[r],e[r],o[r]):e[r]!==o[r]&&(t[r]=e[r]):t[r]=e[r];for(const r in o)e&&r in e||delete t[r]}function L(t){if(t||(t=o()||void 0),t)return t.appContext.config.globalProperties.$metaManager}function q(t,e){const s=o()||void 0;if(!e&&s&&(e=L(s)),!e)throw new Error("No manager or current instance");r(t)&&(n(t,((t,e)=>{_(i.meta,t,e)})),t=t.value);const i=e.addMeta(t,s);return i}function T(){return s(z)}const I=i({name:"Metainfo",inheritAttrs:!1,setup:(t,{slots:e})=>()=>{const t=L();if(t)return t.render({slots:e})}}),B=c({});function D(t,e,o,r){const n=b(r)?r:[r];t?o.endsWith("Attrs")||n.forEach((t=>{t.props||(t.props={}),t.props["data-vm-ssr"]=!0})):n.forEach(((t,e)=>{t.type===l&&n.splice(e,1)})),e[o]||(e[o]=[]),e[o].push(...n)}const H=(t=!1,e,o)=>J.create(t,e||m,o||p);class J{constructor(t,e,o,r){this.isSSR=!1,this.ssrCleanedUp=!1,this.isSSR=t,this.config=e,this.target=o,r&&"setup"in r&&g(r.setup)&&(this.resolver=r)}install(t){t.component("Metainfo",I),t.config.globalProperties.$metaManager=this,t.provide(z,B)}addMeta(t,e){e||(e=o()||void 0);const r={removed:[]},n={vm:e},{resolver:s}=this;s&&s.setup&&s.setup(n);const i=this.target.addSource(t,n,!0),c=t=>this.unmount(!!t,i,r,e);return e&&a(c),{meta:i,onRemoved:t=>r.removed.push(t),unmount:c}}unmount(t,e,o,r){if(r){const{$el:n}=r.proxy;if(n&&n.offsetParent){let r=new MutationObserver((s=>{for(const{removedNodes:i}of s)i&&i.forEach((s=>{s===n&&r&&(r.disconnect(),r=void 0,this.reallyUnmount(t,e,o))}))}));return void r.observe(n.parentNode,{childList:!0})}}this.reallyUnmount(t,e,o)}async reallyUnmount(t,e,o){this.target.delSource(e),!t&&o&&await Promise.all(o.removed.map((t=>t())))}render({slots:t}={}){const{isSSR:o}=this;o||this.ssrCleanedUp||(this.ssrCleanedUp=!0,window.addEventListener("DOMContentLoaded",(()=>{const t=document.querySelectorAll("[data-vm-ssr]");t&&t.length&&t.forEach((t=>t.parentNode&&t.parentNode.removeChild(t)))}),{once:!0}));const r={};for(const e in B){const n=this.config[e]||{};let s=M({isSSR:o,metainfo:B,slots:t},e,B[e],n);if(!s)continue;b(s)||(s=[s]);let i="base"!==e&&B[e].to;!i&&"to"in n&&(i=n.to),!i&&"attributesFor"in n&&(i=e);for(const{to:t,vnode:e}of s)D(this.isSSR,r,t||i||"head",e)}if(t)for(const e in t){const o="default"===e?"head":e;if("head"!==o&&"body"!==o)continue;const n=t[e];g(n)&&D(this.isSSR,r,o,n({metainfo:B}))}return Object.keys(r).map((t=>e(u,{to:t},r[t])))}}J.create=(t,e,o)=>{const r=((t,e)=>{const o=[],r={active:e,resolve:t,sources:o},n=()=>O(r);return{context:r,compute:n,addSource:(t,e,o=!1)=>{const s=x(r,t,e||{});return o&&n(),s},delSource:(t,e=!0)=>{const r=o.findIndex((e=>e===t||e[w]===t));return r>-1&&(o.splice(r,1),e&&n(),!0)}}})(((t,e,r,n,s)=>g(o)?o(t,e,r,n,s):o.resolve(t,e,r,n,s)),B);return new J(t,e,r,o)};export{H as createMetaManager,p as deepestResolver,m as defaultConfig,L as getCurrentManager,f as resolveOption,T as useActiveMeta,q as useMeta}; diff --git a/dist/vue-meta.esm-bundler.js b/dist/vue-meta.esm-bundler.js index 98f302c..2bcf19c 100644 --- a/dist/vue-meta.esm-bundler.js +++ b/dist/vue-meta.esm-bundler.js @@ -1,12 +1,12 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors * @license MIT */ -import { markRaw, h, getCurrentInstance, isProxy, watch, inject, defineComponent, reactive, onUnmounted, Teleport } from 'vue'; +import { markRaw, h, getCurrentInstance, isProxy, watch, inject, defineComponent, reactive, onUnmounted, Teleport, Comment } from 'vue'; const resolveOption = (predicament, initialValue) => (options, contexts) => { let resolvedIndex = -1; @@ -23,7 +23,7 @@ const resolveOption = (predicament, initialValue) => (options, contexts) => { } }; -function setup(context) { +const setup = (context) => { let depth = 0; if (context.vm) { let { vm } = context; @@ -35,7 +35,7 @@ function setup(context) { } while (vm && vm.parent && vm !== vm.root); } context.depth = depth; -} +}; const resolve = resolveOption((currentValue, context) => { const { depth } = context; if (!currentValue || depth > currentValue) { @@ -199,7 +199,7 @@ function clone(v) { const pluck = (collection, key, callback) => { const plucked = []; for (const row of collection) { - if (key in row) { + if (row && key in row) { plucked.push(row[key]); if (callback) { callback(row); @@ -226,13 +226,23 @@ const allKeys = (source, ...sources) => { // TODO: add check for consistent types for each key (dev only) return keys; }; -const recompute = (context, sources, target, path = []) => { - if (!path.length) { - if (!target) { - target = context.active; - } - if (!sources) { - sources = context.sources; +const recompute = (context, path = [], target, sources) => { + const setTargetAndSources = !target && !sources; + if (setTargetAndSources) { + ({ active: target, sources } = context); + if (path.length) { + for (let i = 0; i < path.length; i++) { + const seg = path[i]; + if (!target || !target[seg]) { + if (("development" !== 'production')) { + // eslint-disable-next-line no-console + console.error(`recompute: segment ${seg} not found on target`, path, target); + } + return; + } + target = target[seg]; + sources = sources.map(source => source[seg]).filter(Boolean); + } } } if (!target || !sources) { @@ -249,7 +259,15 @@ const recompute = (context, sources, target, path = []) => { for (const key of keys) { // This assumes consistent types usages for keys across sources // @ts-ignore - if (isPlainObject(sources[0][key])) { + let isObject = false; + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (source && key in source && source[key] !== undefined) { + isObject = isPlainObject(source[key]); + break; + } + } + if (isObject) { if (!target[key]) { target[key] = {}; } @@ -260,7 +278,7 @@ const recompute = (context, sources, target, path = []) => { keySources.push(source[key]); } } - recompute(context, keySources, target[key], [...path, key]); + recompute(context, [...path, key], target[key], keySources); continue; } // Ensure the target is an array if source is an array and target is empty @@ -274,7 +292,6 @@ const recompute = (context, sources, target, path = []) => { if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('RESOLVED', key, resolved, 'was', target[key]) target[key] = resolved; } }; @@ -305,6 +322,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (!isObject(value)) { return value; } + // Also return a merge proxy for nested objects if (!value[IS_PROXY]) { const keyPath = [...pathSegments, key]; value = createProxy(context, value, resolveContext, keyPath); @@ -342,6 +360,12 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ recompute(context); return success; } + else if (isPlainObject(value)) { + // if an object was assigned to this key make sure to recompute all + // of its individual properies + recompute(context, pathSegments); + return success; + } let keyContexts = []; let keySources; if (isArrayItem) { @@ -353,13 +377,13 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); // Ensure to clone if value is an object, cause sources is an array of - // the sourceProxies not the sources so we could trigger an endless loop when + // the sourceProxies and not the sources so we could trigger an endless loop when // updating a prop on an obj as the prop on the active object refers to // a prop on a proxy if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -372,7 +396,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ }, deleteProperty: (target, key) => { const success = Reflect.deleteProperty(target, key); - // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) if (success) { const isArrayItem = isArray(target); let activeSegmentKey; @@ -381,7 +405,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ let index = 0; for (const segment of pathSegments) { // @ts-ignore - proxies = proxies.map(proxy => proxy[segment]); + proxies = proxies.map(proxy => proxy && proxy[segment]); if (isArrayItem && index === pathSegments.length - 1) { activeSegmentKey = segment; break; @@ -391,7 +415,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ } // Check if the key still exists in one of the sourceProxies, // if so resolve the new value, if not remove the key - if (proxies.some(proxy => (key in proxy))) { + if (proxies.some(proxy => proxy && (key in proxy))) { let keyContexts = []; let keySources; if (isArrayItem) { @@ -405,7 +429,7 @@ const createHandler = (context, resolveContext, pathSegments = []) => ({ if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', resolved) + // console.log('SET VALUE', resolved) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -453,6 +477,7 @@ const createMergedObject = (resolve, active) => { }; }; +const cachedElements = {}; function renderMeta(context, key, data, config) { // console.info('renderMeta', key, data, config) if ('attributesFor' in config) { @@ -581,7 +606,7 @@ function renderTag(context, key, data, config = {}, groupConfig) { // console.info('FINAL TAG', finalTag) // console.log(' ATTRIBUTES', attributes) // console.log(' CONTENT', content) - // // console.log(data, attributes, config) + // console.log(data, attributes, config) if (isRaw && content) { attributes.innerHTML = content; } @@ -595,10 +620,10 @@ function renderTag(context, key, data, config = {}, groupConfig) { function renderAttributes(context, key, data, config) { // console.info('renderAttributes', key, data, config) const { attributesFor } = config; - if (!attributesFor) { + if (!attributesFor || !data) { return; } - { + if (context.isSSR) { // render attributes in a placeholder vnode so Vue // will render the string for us return { @@ -606,6 +631,37 @@ function renderAttributes(context, key, data, config) { vnode: h(`ssr-${attributesFor}`, data) }; } + if (!cachedElements[attributesFor]) { + const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); + if (("development" !== 'production') && !el) { + // eslint-disable-next-line no-console + console.error('Could not find element for selector', attributesFor, ', won\'t render attributes'); + return; + } + if (("development" !== 'production') && el2) { + // eslint-disable-next-line no-console + console.warn('Found multiple elements for selector', attributesFor); + } + cachedElements[attributesFor] = { + el, + attrs: [] + }; + } + const { el, attrs } = cachedElements[attributesFor]; + for (const attr in data) { + let content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + if (isArray(content)) { + content = content.join(','); + } + 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); + } } function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { const slot = slots && slots[slotName]; @@ -654,7 +710,7 @@ function applyDifference(target, newSource, oldSource) { } } for (const key in oldSource) { - if (!(key in newSource)) { + if (!newSource || !(key in newSource)) { delete target[key]; } } @@ -707,9 +763,18 @@ const Metainfo = MetainfoImpl; const ssrAttribute = 'data-vm-ssr'; const active = reactive({}); -function addVnode(teleports, to, vnodes) { +function addVnode(isSSR, teleports, to, vnodes) { const nodes = (isArray(vnodes) ? vnodes : [vnodes]); - if (!to.endsWith('Attrs')) { + if (!isSSR) { + // Comments shouldnt have any use on the client as they are not reactive anyway + nodes.forEach((vnode, idx) => { + if (vnode.type === Comment) { + nodes.splice(idx, 1); + } + }); + // only add ssrAttribute's for real meta tags + } + else if (!to.endsWith('Attrs')) { nodes.forEach((vnode) => { if (!vnode.props) { vnode.props = {}; @@ -722,10 +787,12 @@ function addVnode(teleports, to, vnodes) { } teleports[to].push(...nodes); } -const createMetaManager = (config, resolver) => MetaManager.create(config || defaultConfig, resolver || defaultResolver); +const createMetaManager = (isSSR = false, config, resolver) => MetaManager.create(isSSR, config || defaultConfig, resolver || defaultResolver); class MetaManager { - constructor(config, target, resolver) { + constructor(isSSR, config, target, resolver) { + this.isSSR = false; this.ssrCleanedUp = false; + this.isSSR = isSSR; this.config = config; this.target = target; if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { @@ -745,8 +812,9 @@ class MetaManager { removed: [] }); const resolveContext = { vm }; - if (this.resolver) { - this.resolver.setup(resolveContext); + const { resolver } = this; + if (resolver && resolver.setup) { + resolver.setup(resolveContext); } // TODO: optimize initial compute (once) const meta = this.target.addSource(metadata, resolveContext, true); @@ -793,10 +861,25 @@ class MetaManager { } } render({ slots } = {}) { + // TODO: clean this method + const { isSSR } = this; + // cleanup ssr tags if not yet done + if (!isSSR && !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) { + ssrTags.forEach(el => el.parentNode && el.parentNode.removeChild(el)); + } + }, { once: true }); + } const teleports = {}; for (const key in active) { const config = this.config[key] || {}; - let renderedNodes = renderMeta({ metainfo: active, slots }, key, active[key], config); + let renderedNodes = renderMeta({ isSSR, metainfo: active, slots }, key, active[key], config); if (!renderedNodes) { continue; } @@ -811,7 +894,7 @@ class MetaManager { defaultTo = key; } for (const { to, vnode } of renderedNodes) { - addVnode(teleports, to || defaultTo || 'head', vnode); + addVnode(this.isSSR, teleports, to || defaultTo || 'head', vnode); } } if (slots) { @@ -823,16 +906,17 @@ class MetaManager { } const slot = slots[slotName]; if (isFunction(slot)) { - addVnode(teleports, tagName, slot({ metainfo: active })); + addVnode(this.isSSR, teleports, tagName, slot({ metainfo: active })); } } } return Object.keys(teleports).map((to) => { - return h(Teleport, { to }, teleports[to]); + const teleport = teleports[to]; + return h(Teleport, { to }, teleport); }); } } -MetaManager.create = (config, resolver) => { +MetaManager.create = (isSSR, config, resolver) => { const resolve = (options, contexts, active, key, pathSegments) => { if (isFunction(resolver)) { return resolver(options, contexts, active, key, pathSegments); @@ -841,7 +925,7 @@ MetaManager.create = (config, resolver) => { }; const mergedObject = createMergedObject(resolve, active); // TODO: validate resolver - const manager = new MetaManager(config, mergedObject, resolver); + const manager = new MetaManager(isSSR, config, mergedObject, resolver); return manager; }; diff --git a/dist/vue-meta.global.js b/dist/vue-meta.global.js index aa633d3..d48d7ba 100644 --- a/dist/vue-meta.global.js +++ b/dist/vue-meta.global.js @@ -1,5 +1,5 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors @@ -24,7 +24,7 @@ var VueMeta = (function (exports, vue) { } }; - function setup(context) { + const setup = (context) => { let depth = 0; if (context.vm) { let { vm } = context; @@ -36,7 +36,7 @@ var VueMeta = (function (exports, vue) { } while (vm && vm.parent && vm !== vm.root); } context.depth = depth; - } + }; const resolve = resolveOption((currentValue, context) => { const { depth } = context; if (!currentValue || depth > currentValue) { @@ -200,7 +200,7 @@ var VueMeta = (function (exports, vue) { const pluck = (collection, key, callback) => { const plucked = []; for (const row of collection) { - if (key in row) { + if (row && key in row) { plucked.push(row[key]); if (callback) { callback(row); @@ -227,13 +227,23 @@ var VueMeta = (function (exports, vue) { // TODO: add check for consistent types for each key (dev only) return keys; }; - const recompute = (context, sources, target, path = []) => { - if (!path.length) { - if (!target) { - target = context.active; - } - if (!sources) { - sources = context.sources; + const recompute = (context, path = [], target, sources) => { + const setTargetAndSources = !target && !sources; + if (setTargetAndSources) { + ({ active: target, sources } = context); + if (path.length) { + for (let i = 0; i < path.length; i++) { + const seg = path[i]; + if (!target || !target[seg]) { + { + // eslint-disable-next-line no-console + console.error(`recompute: segment ${seg} not found on target`, path, target); + } + return; + } + target = target[seg]; + sources = sources.map(source => source[seg]).filter(Boolean); + } } } if (!target || !sources) { @@ -250,7 +260,15 @@ var VueMeta = (function (exports, vue) { for (const key of keys) { // This assumes consistent types usages for keys across sources // @ts-ignore - if (isPlainObject(sources[0][key])) { + let isObject = false; + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (source && key in source && source[key] !== undefined) { + isObject = isPlainObject(source[key]); + break; + } + } + if (isObject) { if (!target[key]) { target[key] = {}; } @@ -261,7 +279,7 @@ var VueMeta = (function (exports, vue) { keySources.push(source[key]); } } - recompute(context, keySources, target[key], [...path, key]); + recompute(context, [...path, key], target[key], keySources); continue; } // Ensure the target is an array if source is an array and target is empty @@ -275,7 +293,6 @@ var VueMeta = (function (exports, vue) { if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('RESOLVED', key, resolved, 'was', target[key]) target[key] = resolved; } }; @@ -306,6 +323,7 @@ var VueMeta = (function (exports, vue) { if (!isObject(value)) { return value; } + // Also return a merge proxy for nested objects if (!value[IS_PROXY]) { const keyPath = [...pathSegments, key]; value = createProxy(context, value, resolveContext, keyPath); @@ -343,6 +361,12 @@ var VueMeta = (function (exports, vue) { recompute(context); return success; } + else if (isPlainObject(value)) { + // if an object was assigned to this key make sure to recompute all + // of its individual properies + recompute(context, pathSegments); + return success; + } let keyContexts = []; let keySources; if (isArrayItem) { @@ -354,13 +378,13 @@ var VueMeta = (function (exports, vue) { } let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); // Ensure to clone if value is an object, cause sources is an array of - // the sourceProxies not the sources so we could trigger an endless loop when + // the sourceProxies and not the sources so we could trigger an endless loop when // updating a prop on an obj as the prop on the active object refers to // a prop on a proxy if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -373,7 +397,7 @@ var VueMeta = (function (exports, vue) { }, deleteProperty: (target, key) => { const success = Reflect.deleteProperty(target, key); - // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) if (success) { const isArrayItem = isArray(target); let activeSegmentKey; @@ -382,7 +406,7 @@ var VueMeta = (function (exports, vue) { let index = 0; for (const segment of pathSegments) { // @ts-ignore - proxies = proxies.map(proxy => proxy[segment]); + proxies = proxies.map(proxy => proxy && proxy[segment]); if (isArrayItem && index === pathSegments.length - 1) { activeSegmentKey = segment; break; @@ -392,7 +416,7 @@ var VueMeta = (function (exports, vue) { } // Check if the key still exists in one of the sourceProxies, // if so resolve the new value, if not remove the key - if (proxies.some(proxy => (key in proxy))) { + if (proxies.some(proxy => proxy && (key in proxy))) { let keyContexts = []; let keySources; if (isArrayItem) { @@ -406,7 +430,7 @@ var VueMeta = (function (exports, vue) { if (isPlainObject(resolved)) { resolved = clone(resolved); } - // console.log('SET VALUE', resolved) + // console.log('SET VALUE', resolved) if (isArrayItem && activeSegmentKey) { active[activeSegmentKey] = resolved; } @@ -583,7 +607,7 @@ var VueMeta = (function (exports, vue) { // console.info('FINAL TAG', finalTag) // console.log(' ATTRIBUTES', attributes) // console.log(' CONTENT', content) - // // console.log(data, attributes, config) + // console.log(data, attributes, config) if (isRaw && content) { attributes.innerHTML = content; } @@ -597,9 +621,17 @@ var VueMeta = (function (exports, vue) { function renderAttributes(context, key, data, config) { // console.info('renderAttributes', key, data, config) const { attributesFor } = config; - if (!attributesFor) { + if (!attributesFor || !data) { return; } + if (context.isSSR) { + // render attributes in a placeholder vnode so Vue + // will render the string for us + return { + to: '', + vnode: vue.h(`ssr-${attributesFor}`, data) + }; + } if (!cachedElements[attributesFor]) { const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); if (!el) { @@ -618,7 +650,10 @@ var VueMeta = (function (exports, vue) { } const { el, attrs } = cachedElements[attributesFor]; for (const attr in data) { - const content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + let content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + if (isArray(content)) { + content = content.join(','); + } el.setAttribute(attr, content || ''); if (!attrs.includes(attr)) { attrs.push(attr); @@ -676,7 +711,7 @@ var VueMeta = (function (exports, vue) { } } for (const key in oldSource) { - if (!(key in newSource)) { + if (!newSource || !(key in newSource)) { delete target[key]; } } @@ -729,9 +764,9 @@ var VueMeta = (function (exports, vue) { const ssrAttribute = 'data-vm-ssr'; const active = vue.reactive({}); - function addVnode(teleports, to, vnodes) { + function addVnode(isSSR, teleports, to, vnodes) { const nodes = (isArray(vnodes) ? vnodes : [vnodes]); - { + if (!isSSR) { // Comments shouldnt have any use on the client as they are not reactive anyway nodes.forEach((vnode, idx) => { if (vnode.type === vue.Comment) { @@ -740,15 +775,25 @@ var VueMeta = (function (exports, vue) { }); // only add ssrAttribute's for real meta tags } + else if (!to.endsWith('Attrs')) { + nodes.forEach((vnode) => { + if (!vnode.props) { + vnode.props = {}; + } + vnode.props[ssrAttribute] = true; + }); + } if (!teleports[to]) { teleports[to] = []; } teleports[to].push(...nodes); } - const createMetaManager = (config, resolver) => MetaManager.create(config || defaultConfig, resolver || defaultResolver); + const createMetaManager = (isSSR = false, config, resolver) => MetaManager.create(isSSR, config || defaultConfig, resolver || defaultResolver); class MetaManager { - constructor(config, target, resolver) { + constructor(isSSR, config, target, resolver) { + this.isSSR = false; this.ssrCleanedUp = false; + this.isSSR = isSSR; this.config = config; this.target = target; if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { @@ -768,8 +813,9 @@ var VueMeta = (function (exports, vue) { removed: [] }); const resolveContext = { vm }; - if (this.resolver) { - this.resolver.setup(resolveContext); + const { resolver } = this; + if (resolver && resolver.setup) { + resolver.setup(resolveContext); } // TODO: optimize initial compute (once) const meta = this.target.addSource(metadata, resolveContext, true); @@ -817,8 +863,9 @@ var VueMeta = (function (exports, vue) { } render({ slots } = {}) { // TODO: clean this method + const { isSSR } = this; // cleanup ssr tags if not yet done - if (!this.ssrCleanedUp) { + if (!isSSR && !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 @@ -826,14 +873,14 @@ var VueMeta = (function (exports, vue) { window.addEventListener('DOMContentLoaded', () => { const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`); if (ssrTags && ssrTags.length) { - Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el)); + ssrTags.forEach(el => el.parentNode && el.parentNode.removeChild(el)); } - }); + }, { once: true }); } const teleports = {}; for (const key in active) { const config = this.config[key] || {}; - let renderedNodes = renderMeta({ metainfo: active, slots }, key, active[key], config); + let renderedNodes = renderMeta({ isSSR, metainfo: active, slots }, key, active[key], config); if (!renderedNodes) { continue; } @@ -848,7 +895,7 @@ var VueMeta = (function (exports, vue) { defaultTo = key; } for (const { to, vnode } of renderedNodes) { - addVnode(teleports, to || defaultTo || 'head', vnode); + addVnode(this.isSSR, teleports, to || defaultTo || 'head', vnode); } } if (slots) { @@ -860,16 +907,17 @@ var VueMeta = (function (exports, vue) { } const slot = slots[slotName]; if (isFunction(slot)) { - addVnode(teleports, tagName, slot({ metainfo: active })); + addVnode(this.isSSR, teleports, tagName, slot({ metainfo: active })); } } } return Object.keys(teleports).map((to) => { - return vue.h(vue.Teleport, { to }, teleports[to]); + const teleport = teleports[to]; + return vue.h(vue.Teleport, { to }, teleport); }); } } - MetaManager.create = (config, resolver) => { + MetaManager.create = (isSSR, config, resolver) => { const resolve = (options, contexts, active, key, pathSegments) => { if (isFunction(resolver)) { return resolver(options, contexts, active, key, pathSegments); @@ -878,7 +926,7 @@ var VueMeta = (function (exports, vue) { }; const mergedObject = createMergedObject(resolve, active); // TODO: validate resolver - const manager = new MetaManager(config, mergedObject, resolver); + const manager = new MetaManager(isSSR, config, mergedObject, resolver); return manager; }; diff --git a/dist/vue-meta.global.min.js b/dist/vue-meta.global.min.js index 9ebafbb..d7087c1 100644 --- a/dist/vue-meta.global.min.js +++ b/dist/vue-meta.global.min.js @@ -1,8 +1,8 @@ /** - * vue-meta v3.0.0-alpha.5 + * vue-meta v3.0.0-alpha.6 * (c) 2021 * - Pim (@pimlie) * - All the amazing contributors * @license MIT */ -var VueMeta=function(t,e){"use strict";const r=(t,e)=>(r,n)=>{let o=-1;if(n.reduce(((e,r,n)=>{const s=t(e,r);return s!==e?(o=n,s):e}),e),o>-1)return r[o]};const n=r(((t,e)=>{const{depth:r}=e;return!t||r>t?r:t}));var o=Object.freeze({__proto__:null,setup:function(t){let e=0;if(t.vm){let{vm:r}=t;do{r.parent&&(e++,r=r.parent)}while(r&&r.parent&&r!==r.root)}t.depth=e},resolve:n});const s={body:{tag:"script",to:"body"},base:{valueAttribute:"href"},charset:{tag:"meta",nameless:!0,valueAttribute:"charset"},description:{tag:"meta"},og:{group:!0,namespacedAttribute:!0,tag:"meta",keyAttribute:"property"},twitter:{group:!0,namespacedAttribute:!0,tag:"meta"},htmlAttrs:{attributesFor:"html"},headAttrs:{attributesFor:"head"},bodyAttrs:{attributesFor:"body"}},i={title:{attributes:!1},base:{contentAsAttribute:!0,attributes:["href","target"]},meta:{contentAsAttribute:!0,keyAttribute:"name",attributes:["content","name","http-equiv","charset"]},link:{contentAsAttribute:!0,attributes:["href","crossorigin","rel","media","integrity","hreflang","type","referrerpolicy","sizes","imagesrcset","imagesizes","as","color"]},style:{attributes:["media"]},script:{attributes:["src","type","nomodule","async","defer","crossorigin","integrity","referrerpolicy"]},noscript:{attributes:!1}};Object.freeze({}),Object.freeze([]);const c=Array.isArray,a=t=>"function"==typeof t,u=t=>"string"==typeof t,l=t=>null!==t&&"object"==typeof t,f=Object.prototype.toString,d=t=>"[object Object]"===f.call(t),p=Symbol("kIsProxy"),m=Symbol("kProxySources"),b=Symbol("kProxyTarget"),h=Symbol("kResolveContext");function g(t){if(c(t))return t.map(g);if(l(t)){const e={};for(const r in t)e[r]="context"===r?t[r]:g(t[r]);return e}return t}const y=(t,e,r)=>{const n=[];for(const o of t)e in o&&(n.push(o[e]),r&&r(o));return n},v=(t,e,r,n=[])=>{if(n.length||(r||(r=t.active),e||(e=t.sources)),!r||!e)return;const o=((t,...e)=>{const r=t?Object.keys(t):[];if(e)for(const t of e)if(t&&l(t))for(const e in t)r.includes(e)||r.push(e);return r})(...e),s=Object.keys(r);for(const t of s)o.includes(t)||delete r[t];for(const s of o){if(d(e[0][s])){r[s]||(r[s]={});const o=[];for(const t of e)s in t&&o.push(t[s]);v(t,o,r[s],[...n,s]);continue}!r[s]&&c(e[0][s])&&(r[s]=[]);const o=[],i=y(e,s,(t=>o.push(t[h])));let a=t.resolve(i,o,r[s],s,n);d(a)&&(a=g(a)),r[s]=a}},A=(t,r,n,o=[])=>{const s=S(t,n,o),i=e.markRaw(new Proxy(r,s));return!o.length&&t.sources&&t.sources.push(i),i},S=(t,e,r=[])=>({get:(n,o,s)=>{if(o===p)return!0;if(o===m)return t.sources;if(o===b)return n;if(o===h)return e;let i=Reflect.get(n,o,s);if(!l(i))return i;if(!i[p]){const s=[...r,o];i=A(t,i,e,s),Reflect.set(n,o,i)}return i},set:(e,n,o)=>{const s=Reflect.set(e,n,o);if(s){const o=c(e);let i,a=!1,{sources:u,active:l}=t,f=0;for(const t of r){if(u=y(u,t),o&&f===r.length-1){i=t;break}c(l)&&(a=!0),l=l[t],f++}if(a)return v(t),s;let p,m=[];o?(p=u,m=u.map((t=>t[h]))):p=y(u,n,(t=>m.push(t[h])));let b=t.resolve(p,m,l,n,r);d(b)&&(b=g(b)),o&&i?l[i]=b:l[n]=b}return s},deleteProperty:(e,n)=>{const o=Reflect.deleteProperty(e,n);if(o){const o=c(e);let s,i=t.sources,a=t.active,u=0;for(const t of r){if(i=i.map((e=>e[t])),o&&u===r.length-1){s=t;break}a=a[t],u++}if(i.some((t=>n in t))){let e,c=[];o?(e=i,c=i.map((t=>t[h]))):e=y(i,n,(t=>c.push(t[h])));let u=t.resolve(e,c,a,n,r);d(u)&&(u=g(u)),o&&s?a[s]=u:a[n]=u}else delete a[n]}return o}}),k={};function w(t,e,r,n){return"attributesFor"in n?function(t,e,r,n){const{attributesFor:o}=n;if(!o)return;if(!k[o]){const[t,e]=Array.from(document.querySelectorAll(o));if(!t)return void console.error("Could not find element for selector",o,", won't render attributes");e&&console.warn("Found multiple elements for selector",o),k[o]={el:t,attrs:[]}}const{el:s,attrs:i}=k[o];for(const n in r){const o=M(t,`${e}(${n})`,r[n],r);s.setAttribute(n,o||""),i.includes(n)||i.push(n)}const c=i.filter((t=>!r[t]));for(const t of c)s.removeAttribute(t)}(t,e,r,n):"group"in n?function(t,e,r,n){if(c(r))return console.warn("Specifying an array for group properties isnt supported"),[];return Object.keys(r).map((o=>{const s={group:e,data:r};if(n.namespaced)s.tagNamespace=!0===n.namespaced?e:n.namespaced;else if(n.namespacedAttribute){const t=!0===n.namespacedAttribute?e:n.namespacedAttribute;s.fullName=`${t}:${o}`,s.slotName=`${t}(${o})`}return C(t,e,r[o],n,s)})).flat()}(t,e,r,n):C(t,e,r,n)}function C(t,r,n,o={},s){const a=["content","json","rawContent"],l=t=>function(t,e){for(const r of t){const t=i[r];if(r&&t)return t[e]}}([f,o.tag],t);if(c(n))return n.map((e=>C(t,r,e,o,s))).flat();const{tag:f=o.tag||r}=n;let d="",p=!1,m=!1;if(u(n))d=n;else if(n.children&&c(n.children))p=!0,d=n.children.map((e=>{const n=C(t,r,e,o,s);return c(n)?n.map((({vnode:t})=>t)):n.vnode}));else{let t=0;for(const e of a){if(!d&&n[e]){d=1===t?JSON.stringify(n[e]):n[e],m=t>1;break}t++}}const b=s&&s.fullName||r,h=s&&s.slotName||r;let{attrs:g}=n;if(g||"object"!=typeof n)g||(g={});else{g={...n},delete g.tag,delete g.children,delete g.to;for(const t of a)delete g[t]}if(p)d=M(t,h,d,n);else{const e=!!l("contentAsAttribute");let{valueAttribute:r}=o;if(!r&&e){const[t]=l("attributes");r=u(e)?e:t}if(r){const{nameless:e,keyAttribute:n}=o;e||n&&(g[n]=b),g[r]=M(t,h,g[r]||d,s),d=""}else d=M(t,h,d,n)}const y=s&&s.tagNamespace?`${s.tagNamespace}:${f}`:f;m&&d&&(g.innerHTML=d);const v=e.h(y,g,d||void 0);return{to:n.to,vnode:v}}function M({metainfo:t,slots:e},r,n,o){const s=e&&e[r];if(!s||!a(s))return n;const i={content:n,metainfo:t};if(o&&o.group){const{group:t,data:e}=o;i[t]=e}const c=s(i);if(c&&c.length){const{children:t}=c[0];return t?t.toString():""}return n}const j="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,O=(t=>j?Symbol("[vue-meta]: "+t):"[vue-meta]: "+t)("meta_active");function N(t,e,r){for(const n in e)n in r?l(t[n])?N(t[n],e[n],r[n]):e[n]!==r[n]&&(t[n]=e[n]):t[n]=e[n];for(const n in r)n in e||delete t[n]}function P(t){if(t||(t=e.getCurrentInstance()||void 0),t)return t.appContext.config.globalProperties.$metaManager}const x=e.defineComponent({name:"Metainfo",inheritAttrs:!1,setup:(t,{slots:e})=>()=>{const t=P();if(t)return t.render({slots:e})}}),$=e.reactive({});function R(t,r,n){const o=c(n)?n:[n];o.forEach(((t,r)=>{t.type===e.Comment&&o.splice(r,1)})),t[r]||(t[r]=[]),t[r].push(...o)}class F{constructor(t,e,r){this.ssrCleanedUp=!1,this.config=t,this.target=e,r&&"setup"in r&&a(r.setup)&&(this.resolver=r)}install(t){t.component("Metainfo",x),t.config.globalProperties.$metaManager=this,t.provide(O,$)}addMeta(t,r){r||(r=e.getCurrentInstance()||void 0);const n={removed:[]},o={vm:r};this.resolver&&this.resolver.setup(o);const s=this.target.addSource(t,o,!0),i=t=>this.unmount(!!t,s,n,r);return r&&e.onUnmounted(i),{meta:s,onRemoved:t=>n.removed.push(t),unmount:i}}unmount(t,e,r,n){if(n){const{$el:o}=n.proxy;if(o&&o.offsetParent){let n=new MutationObserver((s=>{for(const{removedNodes:i}of s)i&&i.forEach((s=>{s===o&&n&&(n.disconnect(),n=void 0,this.reallyUnmount(t,e,r))}))}));return void n.observe(o.parentNode,{childList:!0})}}this.reallyUnmount(t,e,r)}async reallyUnmount(t,e,r){this.target.delSource(e),!t&&r&&await Promise.all(r.removed.map((t=>t())))}render({slots:t}={}){this.ssrCleanedUp||(this.ssrCleanedUp=!0,window.addEventListener("DOMContentLoaded",(()=>{const t=document.querySelectorAll("[data-vm-ssr]");t&&t.length&&Array.from(t).forEach((t=>t.parentNode&&t.parentNode.removeChild(t)))})));const r={};for(const e in $){const n=this.config[e]||{};let o=w({metainfo:$,slots:t},e,$[e],n);if(!o)continue;c(o)||(o=[o]);let s="base"!==e&&$[e].to;!s&&"to"in n&&(s=n.to),!s&&"attributesFor"in n&&(s=e);for(const{to:t,vnode:e}of o)R(r,t||s||"head",e)}if(t)for(const e in t){const n="default"===e?"head":e;if("head"!==n&&"body"!==n)continue;const o=t[e];a(o)&&R(r,n,o({metainfo:$}))}return Object.keys(r).map((t=>e.h(e.Teleport,{to:t},r[t])))}}return F.create=(t,e)=>{const r=((t,e)=>{const r=[],n={active:e,resolve:t,sources:r},o=()=>v(n);return{context:n,compute:o,addSource:(t,e,r=!1)=>{const s=A(n,t,e||{});return r&&o(),s},delSource:(t,e=!0)=>{const n=r.findIndex((e=>e===t||e[b]===t));return n>-1&&(r.splice(n,1),e&&o(),!0)}}})(((t,r,n,o,s)=>a(e)?e(t,r,n,o,s):e.resolve(t,r,n,o,s)),$);return new F(t,r,e)},t.createMetaManager=(t,e)=>F.create(t||s,e||o),t.deepestResolver=o,t.defaultConfig=s,t.getCurrentManager=P,t.resolveOption=r,t.useActiveMeta=function(){return e.inject(O)},t.useMeta=function(t,r){const n=e.getCurrentInstance()||void 0;if(!r&&n&&(r=P(n)),!r)throw new Error("No manager or current instance");e.isProxy(t)&&(e.watch(t,((t,e)=>{N(o.meta,t,e)})),t=t.value);const o=r.addMeta(t,n);return o},Object.defineProperty(t,"__esModule",{value:!0}),t}({},Vue); +var VueMeta=function(t,e){"use strict";const r=(t,e)=>(r,o)=>{let n=-1;if(o.reduce(((e,r,o)=>{const s=t(e,r);return s!==e?(n=o,s):e}),e),n>-1)return r[n]},o=r(((t,e)=>{const{depth:r}=e;return!t||r>t?r:t}));var n=Object.freeze({__proto__:null,setup:t=>{let e=0;if(t.vm){let{vm:r}=t;do{r.parent&&(e++,r=r.parent)}while(r&&r.parent&&r!==r.root)}t.depth=e},resolve:o});const s={body:{tag:"script",to:"body"},base:{valueAttribute:"href"},charset:{tag:"meta",nameless:!0,valueAttribute:"charset"},description:{tag:"meta"},og:{group:!0,namespacedAttribute:!0,tag:"meta",keyAttribute:"property"},twitter:{group:!0,namespacedAttribute:!0,tag:"meta"},htmlAttrs:{attributesFor:"html"},headAttrs:{attributesFor:"head"},bodyAttrs:{attributesFor:"body"}},i={title:{attributes:!1},base:{contentAsAttribute:!0,attributes:["href","target"]},meta:{contentAsAttribute:!0,keyAttribute:"name",attributes:["content","name","http-equiv","charset"]},link:{contentAsAttribute:!0,attributes:["href","crossorigin","rel","media","integrity","hreflang","type","referrerpolicy","sizes","imagesrcset","imagesizes","as","color"]},style:{attributes:["media"]},script:{attributes:["src","type","nomodule","async","defer","crossorigin","integrity","referrerpolicy"]},noscript:{attributes:!1}};Object.freeze({}),Object.freeze([]);const c=Array.isArray,a=t=>"function"==typeof t,u=t=>"string"==typeof t,l=t=>null!==t&&"object"==typeof t,f=Object.prototype.toString,d=t=>"[object Object]"===f.call(t),p=Symbol("kIsProxy"),m=Symbol("kProxySources"),h=Symbol("kProxyTarget"),b=Symbol("kResolveContext");function g(t){if(c(t))return t.map(g);if(l(t)){const e={};for(const r in t)e[r]="context"===r?t[r]:g(t[r]);return e}return t}const v=(t,e,r)=>{const o=[];for(const n of t)n&&e in n&&(o.push(n[e]),r&&r(n));return o},y=(t,e=[],r,o)=>{if(!r&&!o&&(({active:r,sources:o}=t),e.length))for(let t=0;tt[n])).filter(Boolean)}if(!r||!o)return;const n=((t,...e)=>{const r=t?Object.keys(t):[];if(e)for(const t of e)if(t&&l(t))for(const e in t)r.includes(e)||r.push(e);return r})(...o),s=Object.keys(r);for(const t of s)n.includes(t)||delete r[t];for(const s of n){let n=!1;for(let t=0;ti.push(t[b])));let u=t.resolve(a,i,r[s],s,e);d(u)&&(u=g(u)),r[s]=u}},S=(t,r,o,n=[])=>{const s=A(t,o,n),i=e.markRaw(new Proxy(r,s));return!n.length&&t.sources&&t.sources.push(i),i},A=(t,e,r=[])=>({get:(o,n,s)=>{if(n===p)return!0;if(n===m)return t.sources;if(n===h)return o;if(n===b)return e;let i=Reflect.get(o,n,s);if(!l(i))return i;if(!i[p]){const s=[...r,n];i=S(t,i,e,s),Reflect.set(o,n,i)}return i},set:(e,o,n)=>{const s=Reflect.set(e,o,n);if(s){const i=c(e);let a,u=!1,{sources:l,active:f}=t,p=0;for(const t of r){if(l=v(l,t),i&&p===r.length-1){a=t;break}c(f)&&(u=!0),f=f[t],p++}if(u)return y(t),s;if(d(n))return y(t,r),s;let m,h=[];i?(m=l,h=l.map((t=>t[b]))):m=v(l,o,(t=>h.push(t[b])));let S=t.resolve(m,h,f,o,r);d(S)&&(S=g(S)),i&&a?f[a]=S:f[o]=S}return s},deleteProperty:(e,o)=>{const n=Reflect.deleteProperty(e,o);if(n){const n=c(e);let s,i=t.sources,a=t.active,u=0;for(const t of r){if(i=i.map((e=>e&&e[t])),n&&u===r.length-1){s=t;break}a=a[t],u++}if(i.some((t=>t&&o in t))){let e,c=[];n?(e=i,c=i.map((t=>t[b]))):e=v(i,o,(t=>c.push(t[b])));let u=t.resolve(e,c,a,o,r);d(u)&&(u=g(u)),n&&s?a[s]=u:a[o]=u}else delete a[o]}return n}}),k={};function j(t,r,o,n){return"attributesFor"in n?function(t,r,o,n){const{attributesFor:s}=n;if(!s||!o)return;if(t.isSSR)return{to:"",vnode:e.h(`ssr-${s}`,o)};if(!k[s]){const[t,e]=Array.from(document.querySelectorAll(s));if(!t)return void console.error("Could not find element for selector",s,", won't render attributes");e&&console.warn("Found multiple elements for selector",s),k[s]={el:t,attrs:[]}}const{el:i,attrs:a}=k[s];for(const e in o){let n=C(t,`${r}(${e})`,o[e],o);c(n)&&(n=n.join(",")),i.setAttribute(e,n||""),a.includes(e)||a.push(e)}const u=a.filter((t=>!o[t]));for(const t of u)i.removeAttribute(t)}(t,r,o,n):"group"in n?function(t,e,r,o){if(c(r))return console.warn("Specifying an array for group properties isnt supported"),[];return Object.keys(r).map((n=>{const s={group:e,data:r};if(o.namespaced)s.tagNamespace=!0===o.namespaced?e:o.namespaced;else if(o.namespacedAttribute){const t=!0===o.namespacedAttribute?e:o.namespacedAttribute;s.fullName=`${t}:${n}`,s.slotName=`${t}(${n})`}return w(t,e,r[n],o,s)})).flat()}(t,r,o,n):w(t,r,o,n)}function w(t,r,o,n={},s){const a=["content","json","rawContent"],l=t=>function(t,e){for(const r of t){const t=i[r];if(r&&t)return t[e]}}([f,n.tag],t);if(c(o))return o.map((e=>w(t,r,e,n,s))).flat();const{tag:f=n.tag||r}=o;let d="",p=!1,m=!1;if(u(o))d=o;else if(o.children&&c(o.children))p=!0,d=o.children.map((e=>{const o=w(t,r,e,n,s);return c(o)?o.map((({vnode:t})=>t)):o.vnode}));else{let t=0;for(const e of a){if(!d&&o[e]){d=1===t?JSON.stringify(o[e]):o[e],m=t>1;break}t++}}const h=s&&s.fullName||r,b=s&&s.slotName||r;let{attrs:g}=o;if(g||"object"!=typeof o)g||(g={});else{g={...o},delete g.tag,delete g.children,delete g.to;for(const t of a)delete g[t]}if(p)d=C(t,b,d,o);else{const e=!!l("contentAsAttribute");let{valueAttribute:r}=n;if(!r&&e){const[t]=l("attributes");r=u(e)?e:t}if(r){const{nameless:e,keyAttribute:o}=n;e||o&&(g[o]=h),g[r]=C(t,b,g[r]||d,s),d=""}else d=C(t,b,d,o)}const v=s&&s.tagNamespace?`${s.tagNamespace}:${f}`:f;m&&d&&(g.innerHTML=d);const y=e.h(v,g,d||void 0);return{to:o.to,vnode:y}}function C({metainfo:t,slots:e},r,o,n){const s=e&&e[r];if(!s||!a(s))return o;const i={content:o,metainfo:t};if(n&&n.group){const{group:t,data:e}=n;i[t]=e}const c=s(i);if(c&&c.length){const{children:t}=c[0];return t?t.toString():""}return o}const M="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,R=(t=>M?Symbol("[vue-meta]: "+t):"[vue-meta]: "+t)("meta_active");function O(t,e,r){for(const o in e)o in r?l(t[o])?O(t[o],e[o],r[o]):e[o]!==r[o]&&(t[o]=e[o]):t[o]=e[o];for(const o in r)e&&o in e||delete t[o]}function N(t){if(t||(t=e.getCurrentInstance()||void 0),t)return t.appContext.config.globalProperties.$metaManager}const $=e.defineComponent({name:"Metainfo",inheritAttrs:!1,setup:(t,{slots:e})=>()=>{const t=N();if(t)return t.render({slots:e})}}),P="data-vm-ssr",x=e.reactive({});function F(t,r,o,n){const s=c(n)?n:[n];t?o.endsWith("Attrs")||s.forEach((t=>{t.props||(t.props={}),t.props[P]=!0})):s.forEach(((t,r)=>{t.type===e.Comment&&s.splice(r,1)})),r[o]||(r[o]=[]),r[o].push(...s)}class U{constructor(t,e,r,o){this.isSSR=!1,this.ssrCleanedUp=!1,this.isSSR=t,this.config=e,this.target=r,o&&"setup"in o&&a(o.setup)&&(this.resolver=o)}install(t){t.component("Metainfo",$),t.config.globalProperties.$metaManager=this,t.provide(R,x)}addMeta(t,r){r||(r=e.getCurrentInstance()||void 0);const o={removed:[]},n={vm:r},{resolver:s}=this;s&&s.setup&&s.setup(n);const i=this.target.addSource(t,n,!0),c=t=>this.unmount(!!t,i,o,r);return r&&e.onUnmounted(c),{meta:i,onRemoved:t=>o.removed.push(t),unmount:c}}unmount(t,e,r,o){if(o){const{$el:n}=o.proxy;if(n&&n.offsetParent){let o=new MutationObserver((s=>{for(const{removedNodes:i}of s)i&&i.forEach((s=>{s===n&&o&&(o.disconnect(),o=void 0,this.reallyUnmount(t,e,r))}))}));return void o.observe(n.parentNode,{childList:!0})}}this.reallyUnmount(t,e,r)}async reallyUnmount(t,e,r){this.target.delSource(e),!t&&r&&await Promise.all(r.removed.map((t=>t())))}render({slots:t}={}){const{isSSR:r}=this;r||this.ssrCleanedUp||(this.ssrCleanedUp=!0,window.addEventListener("DOMContentLoaded",(()=>{const t=document.querySelectorAll("[data-vm-ssr]");t&&t.length&&t.forEach((t=>t.parentNode&&t.parentNode.removeChild(t)))}),{once:!0}));const o={};for(const e in x){const n=this.config[e]||{};let s=j({isSSR:r,metainfo:x,slots:t},e,x[e],n);if(!s)continue;c(s)||(s=[s]);let i="base"!==e&&x[e].to;!i&&"to"in n&&(i=n.to),!i&&"attributesFor"in n&&(i=e);for(const{to:t,vnode:e}of s)F(this.isSSR,o,t||i||"head",e)}if(t)for(const e in t){const r="default"===e?"head":e;if("head"!==r&&"body"!==r)continue;const n=t[e];a(n)&&F(this.isSSR,o,r,n({metainfo:x}))}return Object.keys(o).map((t=>e.h(e.Teleport,{to:t},o[t])))}}return U.create=(t,e,r)=>{const o=((t,e)=>{const r=[],o={active:e,resolve:t,sources:r},n=()=>y(o);return{context:o,compute:n,addSource:(t,e,r=!1)=>{const s=S(o,t,e||{});return r&&n(),s},delSource:(t,e=!0)=>{const o=r.findIndex((e=>e===t||e[h]===t));return o>-1&&(r.splice(o,1),e&&n(),!0)}}})(((t,e,o,n,s)=>a(r)?r(t,e,o,n,s):r.resolve(t,e,o,n,s)),x);return new U(t,e,o,r)},t.createMetaManager=(t=!1,e,r)=>U.create(t,e||s,r||n),t.deepestResolver=n,t.defaultConfig=s,t.getCurrentManager=N,t.resolveOption=r,t.useActiveMeta=function(){return e.inject(R)},t.useMeta=function(t,r){const o=e.getCurrentInstance()||void 0;if(!r&&o&&(r=N(o)),!r)throw new Error("No manager or current instance");e.isProxy(t)&&(e.watch(t,((t,e)=>{O(n.meta,t,e)})),t=t.value);const n=r.addMeta(t,o);return n},Object.defineProperty(t,"__esModule",{value:!0}),t}({},Vue); diff --git a/package.json b/package.json index a26e4b5..88a33fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-meta", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "Manage HTML metadata in Vue.js components with SSR support", "main": "dist/vue-meta.cjs.js", "browser": "dist/vue-meta.esm-browser.min.js",