2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-05-17 05:09:38 +03:00

chore(release): 3.0.0-alpha.6

This commit is contained in:
pimlie
2021-05-17 00:12:16 +00:00
parent 1d847870e9
commit a8d6f0ab0e
11 changed files with 571 additions and 207 deletions
+16
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
import { App } from 'vue';
import { SSRContext } from '@vue/server-renderer';
declare function renderMetaToString(app: App, ctx?: SSRContext): Promise<SSRContext>;
export { renderMetaToString };
+120 -36
View File
@@ -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;
};
+107 -36
View File
@@ -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;
};
+22 -15
View File
@@ -1,3 +1,13 @@
/**
* vue-meta v3.0.0-alpha.6
* (c) 2021
* - Pim (@pimlie)
* - All the amazing contributors
* @license MIT
*/
/// <reference path="ssr.d.ts" />
import { App, ComponentInternalInstance, Slots, VNode } from 'vue';
declare const IS_PROXY: unique symbol;
@@ -35,15 +45,16 @@ declare type MergedObjectBuilder<T> = {
delSource: (sourceOrProxy: T | MergeSource<T>, 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<MetaSource>;
resolver?: MetaResolverSetup;
resolver?: MetaResolver;
ssrCleanedUp: boolean;
constructor(config: MetaConfig, target: MergedObjectBuilder<MetaSource>, resolver: MetaResolver | ResolveMethod);
static create: createMetaManagerMethod;
constructor(isSSR: boolean, config: MetaConfig, target: MergedObjectBuilder<MetaSource>, 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<MetaResolver, {
setup: MetaResolveSetup;
}>;
declare type MetaResolverSetup = Required<MetaResolver>;
/**
* @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<any, MergeResolveContextDeepest>;
declare const setup: MetaResolveSetup;
declare const resolve: ResolveMethod<any, ResolveContext>;
declare const deepest_d_setup: typeof setup;
declare const deepest_d_resolve: typeof resolve;
+87 -39
View File
@@ -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;
};
+2 -2
View File
File diff suppressed because one or more lines are too long
+121 -37
View File
@@ -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;
};
+87 -39
View File
@@ -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;
};
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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",