diff --git a/examples/vue-router/app.js b/examples/vue-router/app.js
index 8355d92..376feb3 100644
--- a/examples/vue-router/app.js
+++ b/examples/vue-router/app.js
@@ -1,11 +1,18 @@
import Vue from 'vue'
import VueMeta from 'vue-meta'
import Router from 'vue-router'
+import { createMixin } from './vue-meta-next/mixin'
Vue.use(Router)
-Vue.use(VueMeta, {
- refreshOnceOnNavigation: true
+/*Vue.use(VueMeta, {
+ refreshOnceOnNavigation: false,
+ //waitOnDestroyed: false,
+ //debounceWait: 0
})
+/**/
+
+Vue.mixin(createMixin())
+/**/
let metaUpdated = 'no'
const ChildComponent = {
@@ -15,20 +22,25 @@ const ChildComponent = {
You're looking at the {{ page }} page
Has metaInfo been updated due to navigation? {{ metaUpdated }}
`,
- metaInfo () {
- return {
- title: `${this.page} - ${this.date && this.date.toTimeString()}`,
- bodyAttrs: {
- class: 'child-component'
- },
- afterNavigation () {
- metaUpdated = 'yes'
- }
- }
+ metaInfo: {
+ title() {
+ return `${this.page} - ${this.date && this.date.toTimeString()}`
+ },
+ /*bodyAttrs: {
+ class: 'child-component'
+ },*/
+ meta: [
+ function () { return { vmid: 'descr', name: 'description', content: `description at ${this && this.date2 && this.date2.toTimeString()}` } },
+ { name: 'og:description', content: `og:description at ${this && this.date2 && this.date2.toTimeString()}` }
+ ],
+ /*afterNavigation () {
+ metaUpdated = 'yes'
+ }*/
},
data () {
return {
date: null,
+ date2: null,
metaUpdated
}
},
@@ -36,9 +48,14 @@ const ChildComponent = {
this.interval = setInterval(() => {
this.date = new Date()
}, 1000)
+
+ this.interval2 = setInterval(() => {
+ this.date2 = new Date()
+ }, 3000)
},
destroyed () {
clearInterval(this.interval)
+ clearInterval(this.interval2)
}
}
@@ -65,23 +82,47 @@ const router = new Router({
]
})
+const getTime = () => window.performance.now()
+
+let navtime
+let navtimes = []
+
const App = {
router,
+ metaInfo() {
+ return {
+ meta: [
+ { vmid: 'descr', name: 'description', content: 'JAHASJDKASD' }
+ ]
+ }
+ },
+ created() {
+ router.beforeEach((to, from, next) => {
+ navtime = getTime()
+ console.log('before', navtime)
+ next()
+ })
+
+ router.afterEach((to, from) => {
+ console.log('after', getTime())
+ navtimes.push((getTime()) - navtime)
+ console.log(navtimes.length, navtimes.reduce((acc, v) => (acc + v), 0) / navtimes.length, document.title, '|', top.location.pathname)
+ })
+ },
template: `
vue-router
Home
About
-
-
Inspect Element to see the meta info
`
}
+//
const app = new Vue(App)
-
+/*
const { set, remove } = app.$meta().addApp('custom')
set({
@@ -93,8 +134,22 @@ set({
]
})
setTimeout(() => remove(), 3000)
-
+*/
app.$mount('#app')
+/*
+let i = 0
+const interval = setInterval(() => {
+ router.push(top.location.href.includes('about') ? '/' : '/about')
+ i++
+ if (i == 3) {
+ navtimes.shift()
+ navtimes.shift()
+ }
+ if (i >= 50) {
+ clearInterval(interval)
+ }
+}, 1000)
+/**/
/*
const waitFor = time => new Promise(r => setTimeout(r, time || 1000))
diff --git a/examples/vue-router/vue-meta-next/meta.js b/examples/vue-router/vue-meta-next/meta.js
new file mode 100644
index 0000000..d240ea7
--- /dev/null
+++ b/examples/vue-router/vue-meta-next/meta.js
@@ -0,0 +1,319 @@
+import Vue from 'vue'
+import {
+ sortReverse,
+ sortMetaData,
+ createMetaInfoForTypeKey,
+ createWatcher
+} from './util'
+
+// This object contains the metadata returned by vue components
+// grouped by vmid. metadata without vmid is not listed here.
+// The purpose of this object is to be able to re-assign the vmid
+// when the VueComponent that supplied the vmid with the highest priority
+// gets destroyed
+// -> title is a special case, its always treated as if it has a vmid
+export const allMetaData = {
+ title: [],
+ meta: {}
+}
+
+// This object shadows the metadata that is actually rendered
+// -> title is a special case, its always treated as if it has a vmid
+// -> for meta (and link, script, etc) the signature here is: {
+// any: [] // <- an array with all metadata objects without a vmid
+// [vmid]: {} // <- for each vmid the vmid is used as key with the metadata object as value
+export const displayedMetaData = Vue.observable({
+ title: undefined,
+ meta: {
+ any: []
+ }
+})
+
+// find the meta data with the highest priority, if multiple components exists
+// with the same priority then the lowest component id is used
+function evaluateMetaDataToUse(vm, type, key) {
+ // TODO: add error handling
+ if (key) {
+ const allMetaDataForTypeById = allMetaData[type][key]
+
+ if (allMetaDataForTypeById.length > 1) {
+ allMetaDataForTypeById.sort(sortMetaData)
+ }
+
+ // re-use the previous HTMLElement if possible
+ if (displayedMetaData[type][key]) {
+ // we need to copy the HTMLElement and unwatch function if they exist
+ for (const prop of ['_el', '_unwatch']) {
+ allMetaDataForTypeById[0][prop] = displayedMetaData[type][key][prop]
+ }
+ }
+
+ // add watcher
+ Vue.set(displayedMetaData[type], key, allMetaDataForTypeById[0])
+
+ // check if this a new element that wasnt displayed before, if so
+ // add a watcher to create/modify the HTMLElement and store the
+ // returned unwatcher to prevent memory leaks
+ if (!allMetaDataForTypeById[0]._unwatch) {
+ const unwatch = vm.$root.$watch(
+ `$data.metaInfo.${type}.${key}`,
+ createWatcher(type),
+ { deep: true, immediate: true }
+ )
+
+ allMetaDataForTypeById[0]._unwatch = unwatch
+ }
+ return
+ }
+
+ // TODO: this should only be called for title? maybe base?
+
+ const allMetaDataForTypeById = allMetaData[type]
+ if (allMetaDataForTypeById.length > 1) {
+ // only sort when there is more then 1 element
+ allMetaDataForTypeById.sort(sortMetaData)
+ }
+
+ Vue.set(displayedMetaData, type, allMetaDataForTypeById[0])
+}
+
+// This method is called every time a watcher is triggered on the
+// classic metaInfo / head property of a SFC
+export function updateMetaInfo(vm, data) {
+ for (const type in data) {
+ if (typeof data[type] === 'function') {
+ console.warn(`prop ${type} ignored, function within functions are not supported`)
+ continue
+ }
+ updateMetaInfoForType(vm, data[type], type)
+ }
+}
+
+// To support changes bya single type (meta, link script) only, this method is
+// used as the real updater method. It can also call itself
+// -> nestedIndex: important prop. Its the indicator which groups updates returned
+// from a meta array element item function. The index is 1-based (not 0-base) so
+// we can still check for falsy values, eg:
+// metaInfo: {
+// meta: [ // <- calls updateMetaInfoForType without nestedIndex
+// function () { return {} }, // <- calls updateMetaInfoForType without nestedIndex = 1
+// function () { return {} } // <- calls updateMetaInfoForType without nestedIndex = 2
+// ],
+export function updateMetaInfoForType(vm, data, type, nestedIndex) {
+ nestedIndex = nestedIndex || 0
+
+ const _cid = vm._vueMeta.id
+ const allMetaDataForType = allMetaData[type]
+
+ if (type === 'title') {
+ const index = allMetaDataForType.findIndex(metaData => metaData._cid === _cid)
+
+ if (index === -1) {
+ allMetaDataForType.push({ _cid, prio: vm._vueMeta.depth || _cid, content: data })
+
+ evaluateMetaDataToUse(vm, type)
+ } else {
+ allMetaDataForType[index].content = data
+ }
+
+ return
+ }
+
+ if (!Array.isArray(data)) {
+ console.log(`What is this, an unsupported feature perhaps? Expected array for ${type}`)
+ return
+ }
+
+ const vmidsSeen = []
+
+ for (const [idx, metaData] of Object.entries(data)) {
+ if (typeof metaData === 'function') {
+ const unwatch = vm.$watch(
+ metaData,
+ (newValue) => {
+ // ensure its an array
+ if (!Array.isArray(newValue)) {
+ newValue = [newValue]
+ }
+ updateMetaInfoForType(vm, newValue, type, idx + 1)
+ },
+ { immediate: true }
+ )
+ vm._vueMeta.unwatch.push(unwatch)
+ continue
+ }
+
+ const { vmid } = metaData
+ if (!vmid) {
+ continue
+ }
+
+ vmidsSeen.push(vmid)
+
+ allMetaDataForType[vmid] = allMetaDataForType[vmid] || []
+
+ // TODO: not sure anymore if we need this
+ const _key = createMetaInfoForTypeKey(vm, vmid, nestedIndex)
+ const prio = metaData.prio || vm._vueMeta.depth || _cid
+ const index = allMetaDataForType[vmid].findIndex(metaData => metaData._key === _key)
+
+ // set this before the splice when vmid was already lsited
+ const newOrPrioChanged = index === -1 || allMetaDataForType[vmid][index].prio !== prio
+
+ // we can assign directly to metaData because its the result of a watcher
+ metaData = Object.assign(metaData, {
+ _cid,
+ _nested: nestedIndex,
+ _key,
+ prio
+ })
+
+ if (index === -1) {
+ allMetaDataForType[vmid].push(metaData)
+ } else {
+ // we are going to assign the metadata object to the eixsiting one in allMetaData
+ // TODO: is this really necessary? cant we just copy el/unwatch again and thats it? think so?
+ for (const prop in allMetaDataForType[vmid][index]) {
+ // we need to keep these props and we know they dont
+ if (['_el', '_unwatch'].includes(prop) || prop in metaData) {
+ continue
+ }
+
+ delete allMetaDataForType[vmid][index][prop]
+ }
+
+ Object.assign(allMetaDataForType[vmid][index], metaData)
+ }
+
+ // only re-evaluate which data to show when its new data or the priority has changed
+ if (newOrPrioChanged) {
+ evaluateMetaDataToUse(vm, type, vmid)
+ }
+ }
+
+ // remove old/previous metadata from this component
+ removeMetaDataForType(vm, _cid, type, ({ vmid, _nested }) => {
+ // dont remove if the metadata has a different nesting index
+ if (nestedIndex !== _nested) {
+ return false
+ }
+
+ // remove if metadata doesnt has vmid or when its vmid wasnt listed in this update
+ return !vmid || !vmidsSeen.includes(vmid)
+ })
+
+ for (const metaData of data) {
+ if (typeof metaData === 'function' || metaData.vmid) {
+ // these types have already been handled above, ignore
+ continue
+ }
+
+ // clone metaData!!
+ displayedMetaData[type].any.push({
+ ...metaData,
+ _cid,
+ _nested: nestedIndex
+ })
+ }
+}
+
+export function removeMetaDataForType(vm, componentId, type, filter) {
+ for (const key in displayedMetaData[type]) {
+ if (key === 'any') {
+ // the any key lists all the metadata without a vmid
+ displayedMetaData[type][key]
+ .map((metaData, index) => {
+ if (metaData._cid !== componentId) {
+ return -1
+ }
+
+ if (filter && !filter(metaData)) {
+ return -1
+ }
+
+ return index
+ })
+ .filter(index => index > -1)
+ // reverse sort to make sure array indexes arent changed after splicing
+ .sort(sortReverse)
+ .map(index => {
+ const { _el: el, vmid } = displayedMetaData[type][key][index]
+ if (el) {
+ el.remove()
+ }
+
+ displayedMetaData[type][key].splice(index, 1)
+
+ if (vmid) {
+ evaluateMetaDataToUse(vm, type, vmid)
+ }
+ })
+
+ continue
+ }
+
+ // all other keys are vmid's, check that the currently rendered meta data
+ // belongs to the current component. If so, remove the metadata and
+ // re-assign the rendered vmid to a new value (if possible)
+ const { _cid, _el, _unwatch, vmid } = displayedMetaData[type][key]
+ if (_cid === componentId) {
+ // only remove vmids which we havent seen
+ if (filter && !filter(displayedMetaData[type][key])) {
+ continue
+ }
+
+ if (_el) {
+ _el.remove()
+ }
+
+ if (_unwatch) {
+ _unwatch()
+ }
+
+ displayedMetaData[type][key] = null
+ evaluateMetaDataToUse(vm, type, vmid)
+ }
+ }
+}
+
+// remove all the meta data from a component once it gets destroyed
+export function onComponentDestroyed(vm, componentId) {
+ if (!vm.$options.metaInfo) {
+ return
+ }
+
+ vm._vueMeta.unwatch.forEach(unwatch => unwatch())
+
+ componentId = componentId || vm._vueMeta.id
+
+ for (const type in allMetaData) {
+ if (type === 'title') {
+ const reassignInfo = displayedMetaData[type] && displayedMetaData[type]._cid === componentId
+ const index = allMetaData[type].findIndex(({ _cid }) => _cid === componentId)
+
+ if (index > -1) {
+ allMetaData[type].splice(index, 1)
+
+ if (reassignInfo) {
+ evaluateMetaDataToUse(vm, type)
+ }
+ }
+ continue
+ }
+
+ for (const vmid in allMetaData[type]) {
+ const index = allMetaData[type][vmid].findIndex(({ _cid }) => _cid === componentId)
+ if (index > -1) {
+ allMetaData[type][vmid].splice(index, 1)
+ }
+ }
+ }
+
+ for (const type in displayedMetaData) {
+ if (type === 'title') {
+ continue
+ }
+
+ removeMetaDataForType(vm, componentId, type)
+ }
+}
diff --git a/examples/vue-router/vue-meta-next/mixin.js b/examples/vue-router/vue-meta-next/mixin.js
new file mode 100644
index 0000000..ddc64bb
--- /dev/null
+++ b/examples/vue-router/vue-meta-next/mixin.js
@@ -0,0 +1,90 @@
+import {
+ allMetaData,
+ displayedMetaData,
+ onComponentDestroyed,
+ updateMetaInfo,
+ updateMetaInfoForType
+} from './meta'
+
+import {
+ getComponentDepth,
+ createWatcher
+} from './util'
+
+let vueMetaComponentId = 1
+let watchersAdded = false
+
+export function createMixin() {
+ return {
+ created() {
+ // add global meta watchers on root only once for the full page
+ // TODO: maybe create our own vue-meta Vue instance for that?
+ // -> new Vue instance takes a coupe of ms
+ if (this === this.$root) {
+ if (watchersAdded) {
+ return
+ }
+ watchersAdded = true
+
+ // for easily debugging
+ this.$data.metaInfo = displayedMetaData
+ this.$data.allMetaInfo = allMetaData
+
+ for (const type in displayedMetaData) {
+ if (type === 'title') {
+ this.$watch(
+ `$data.metaInfo.${type}`,
+ createWatcher(type),
+ { deep: true, immediate: true }
+ )
+ continue
+ }
+
+ for (const key in displayedMetaData[type]) {
+ this.$watch(
+ `$data.metaInfo.${type}.${key}`,
+ createWatcher(type),
+ { deep: true, immediate: true }
+ )
+ }
+ }
+ }
+
+ if (this.$options.metaInfo) {
+ this._vueMeta = {
+ id: vueMetaComponentId++,
+ depth: getComponentDepth(this),
+ unwatch: []
+ }
+
+ if (typeof this.$options.metaInfo === 'function') {
+ this.$watch(
+ this.$options.metaInfo,
+ (newValue) => updateMetaInfo(this, newValue),
+ { immediate: true }
+ )
+ return
+ }
+
+ for (const type in this.$options.metaInfo) {
+ const metaInfoForType = this.$options.metaInfo[type]
+
+ if (typeof metaInfoForType === 'function') {
+ this.$watch(
+ metaInfoForType,
+ (newValue) => updateMetaInfoForType(this, newValue, type),
+ { deep: true, immediate: true }
+ )
+
+ continue
+ }
+
+ updateMetaInfoForType(this, metaInfoForType, type)
+ }
+ }
+ },
+ beforeDestroy() {
+ onComponentDestroyed(this)
+ }
+ }
+}
diff --git a/examples/vue-router/vue-meta-next/util.js b/examples/vue-router/vue-meta-next/util.js
new file mode 100644
index 0000000..74f68ed
--- /dev/null
+++ b/examples/vue-router/vue-meta-next/util.js
@@ -0,0 +1,66 @@
+export const internalProps = [
+ 'prio'
+]
+
+export const sortReverse = (a, b) => (b - a)
+export const sortMetaData = (a, b) => {
+ if (a.prio === b.prio) {
+ return a._cid < b._cid ? -1 : 1
+ }
+
+ return a.prio > b.prio ? -1 : 1
+}
+
+export function createMetaInfoForTypeKey(vm, type, vmid, nestedIndex) {
+ return `${vm._vueMeta.id}-${type}${vmid ? '-'.concat(vmid) : ''}${nestedIndex ? '-'.concat(nestedIndex) : ''}`
+}
+
+// the depth of a component is the default meta priority, as in
+// the deeper the component the more important its meta data
+export function getComponentDepth(vm) {
+ let depth = 0
+ let parent = vm
+
+ while (parent !== vm.$root) {
+ depth++
+ parent = parent.$parent
+ }
+
+ return depth
+}
+
+export function updateElement(meta) {
+ const el = meta._el || document.createElement('meta')
+
+ // set non-private props as attribute
+ Object.keys(meta)
+ .filter(key => key[0] !== '_' && !internalProps.includes(key))
+ .forEach(key => {
+ if (!el.hasAttribute(key) || el.getAttribute(key) != meta[key]) { // eslint-disable-line eqeqeq
+ el.setAttribute(key, meta[key])
+ }
+ })
+
+ if (meta._el) {
+ return
+ }
+
+ meta._el = el
+ document.head.appendChild(el)
+}
+
+export function createWatcher(type) {
+ return function metaDataWatcher(newValue) {
+ if (type === 'title') {
+ document.title = (newValue && newValue.content) || ''
+ return
+ }
+
+ if (Array.isArray(newValue)) {
+ newValue.forEach(updateElement)
+ return
+ }
+
+ updateElement(newValue)
+ }
+}