2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-15 10:42:26 +03:00

Merge pull request #324 from pimlie/feat-more-refactor

feat: refresh once on navigation & es build
This commit is contained in:
Sébastien Chopin
2019-03-06 14:52:31 +01:00
committed by GitHub
32 changed files with 2101 additions and 10707 deletions
+11 -1
View File
@@ -1,3 +1,13 @@
{
"presets": ["@babel/preset-env"]
"env": {
"test": {
"presets": [
[ "@babel/env", {
"targets": {
"node": "current"
}
}]
]
}
}
}
+1
View File
@@ -39,6 +39,7 @@ package-lock.json
# built code
lib
es
# examples yarn lock
examples/yarn.lock
+25 -6
View File
@@ -6,12 +6,6 @@
Manage page meta info in Vue 2.0 components. SSR + Streaming supported. Inspired by <a href="https://github.com/nfl/react-helmet">react-helmet</a>.
</h5>
<p align="center">
<a href="https://github.com/feross/standard">
<img src="https://cdn.rawgit.com/feross/standard/master/badge.svg" alt="Standard - JavaScript Style">
</a>
</p>
<p align="center">
<a href="https://github.com/nuxt/vue-meta/releases/latest"><img src="https://img.shields.io/github/release/nuxt/vue-meta.svg" alt="github release"></a> <a href="http://npmjs.org/package/vue-meta"><img src="https://img.shields.io/npm/v/vue-meta.svg" alt="npm version"></a> <a href="https://circleci.com/gh/nuxt/vue-meta/"><img src="https://badgen.net/circleci/github/nuxt/vue-meta" alt="Build Status"></a> <a href="https://codecov.io/gh/nuxt/vue-meta"><img src="https://codecov.io/gh/nuxt/vue-meta/branch/master/graph/badge.svg" alt="codecov"></a><br>
<a href="https://david-dm.org/nuxt/vue-meta"><img src="https://david-dm.org/nuxt/vue-meta/status.svg" alt="dependencies Status"></a> <a href="https://david-dm.org/nuxt/vue-meta?type=dev"><img src="https://david-dm.org/nuxt/vue-meta/dev-status.svg" alt="devDependencies Status"></a><br>
@@ -70,6 +64,8 @@
- [`__dangerouslyDisableSanitizers` ([String])](#__dangerouslydisablesanitizers-string)
- [`__dangerouslyDisableSanitizersByTagID` ({[String]})](#__dangerouslydisablesanitizersbytagid-string)
- [`changed` (Function)](#changed-function)
- [`refreshOnceOnNavigation` (Boolean)](#refreshonceonnavigation-boolean)
- [`afterNavigation` (Function)](#afternavigation-function)
- [How `metaInfo` is Resolved](#how-metainfo-is-resolved)
- [Lists of Tags](#lists-of-tags)
- [Performance](#performance)
@@ -658,6 +654,29 @@ Will be called when the client `metaInfo` updates/changes. Receives the followin
}
```
#### `refreshOnceOnNavigation` (Boolean)
Default `false`. If set to `true` then vue-meta will pause updating `metaInfo` during page navigation and only refresh once when navigation has finished. It does this by adding a global beforeEach and afterEach navigation guard on the vue-router instance.
#### `afterNavigation` (Function)
Will be called when the client `metaInfo` has changed after navigation occured. Receives the following parameters:
- `newInfo` (Object) - The new state of the `metaInfo` object.
> :warning: This option only works when `refreshOnceOnNavigation: true`. Please see the [vue-router example](./examples/vue-router)
`this` context is the component instance `afterNavigation` is defined on.
```js
{
metaInfo: {
afterNavigation (newInfo) {
console.log('Meta info update finished after navigation!')
}
}
}
```
### How `metaInfo` is Resolved
You can define a `metaInfo` property on any component in the tree. Child components that have `metaInfo` will recursively merge their `metaInfo` into the parent context, overwriting any duplicate properties. To better illustrate, consider this component heirarchy:
-7010
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -19,21 +19,21 @@
},
"homepage": "https://github.com/nuxt/vue-meta#readme",
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/core": "^7.3.3",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.3.1",
"babel-loader": "^8.0.5",
"cross-env": "^5.2.0",
"express": "^4.16.4",
"express-urlrewrite": "^1.2.0",
"vue": "^2.6.3",
"vue-loader": "^15.6.2",
"vue": "^2.6.6",
"vue-loader": "^15.6.4",
"vue-meta": "^1.5.8",
"vue-router": "^3.0.2",
"vue-template-compiler": "^2.6.3",
"vue-template-compiler": "^2.6.6",
"vuex": "^3.1.0",
"webpack": "^4.26.1",
"webpack-dev-server": "^3.1.10",
"webpack": "^4.29.5",
"webpack-dev-server": "^3.2.0",
"webpackbar": "^3.1.5"
}
}
+15 -5
View File
@@ -3,20 +3,30 @@ import VueMeta from 'vue-meta'
import Router from 'vue-router'
Vue.use(Router)
Vue.use(VueMeta)
Vue.use(VueMeta, {
refreshOnceOnNavigation: true
})
let metaUpdated = 'no'
const ChildComponent = {
name: `child-component`,
props: ['page'],
template: `<h3>You're looking at the <strong>{{ page }}</strong> page</h3>`,
template: `<div>
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
<p>Has metaInfo been updated? {{ metaUpdated }}</p>
</div>`,
metaInfo () {
return {
title: `${this.page} - ${this.date && this.date.toTimeString()}`
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
afterNavigation() {
metaUpdated = 'yes'
}
}
},
data() {
return {
date: null
date: null,
metaUpdated
};
},
mounted() {
@@ -28,7 +38,7 @@ const ChildComponent = {
// this wrapper function is not a requirement for vue-router,
// just a demonstration that render-function style components also work.
// See https://github.com/declandewet/vue-meta/issues/9 for more info.
// See https://github.com/nuxt/vue-meta/issues/9 for more info.
function view (page) {
return {
name: `section-${page}`,
+2 -7
View File
@@ -22,8 +22,7 @@ module.exports = {
],
testPathIgnorePatterns: [
'node_modules',
'old'
'node_modules'
],
transformIgnorePatterns: [
@@ -39,9 +38,5 @@ module.exports = {
'ts',
'js',
'json'
],
reporters: [
'default'
].concat(process.env.JEST_JUNIT_OUTPUT ? ['jest-junit'] : [])
]
}
+76 -71
View File
@@ -1,73 +1,7 @@
{
"name": "vue-meta",
"description": "Manage page meta info in Vue 2.0 server-rendered apps",
"version": "1.5.8",
"author": "Declan de Wet <declandewet@me.com>",
"bugs": "https://github.com/nuxt/vue-meta/issues",
"scripts": {
"build": "rimraf lib && rollup -c scripts/rollup.config.js",
"codecov": "codecov",
"deploy": "npm version",
"dev": "cd examples && npm run dev && cd ..",
"lint": "eslint src test",
"postdeploy": "git push origin master --follow-tags && npm run release",
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
"predeploy": "git checkout master && git pull -r",
"prerelease": "npm run build",
"preversion": "npm run toc",
"release": "npm publish",
"test": "jest",
"toc": "doctoc README.md --title '# Table of Contents'",
"update-cdn": "babel-node scripts/update-cdn.js"
},
"dependencies": {
"deepmerge": "^3.0.0",
"lodash.isplainobject": "^4.0.6",
"lodash.uniqueid": "^4.0.1"
},
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.1.6",
"@nuxtjs/eslint-config": "^0.0.1",
"@vue/server-test-utils": "^1.0.0-beta.29",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
"babel-loader": "^8.0.4",
"codecov": "^3.1.0",
"doctoc": "^1.4.0",
"eslint": "^5.13.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.2.2",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.1.0",
"jest": "^24.1.0",
"jsdom": "^13.2.0",
"jsdom-global": "^3.0.2",
"rimraf": "^2.6.2",
"rollup": "^1.0.0",
"rollup-plugin-buble": "^0.19.4",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-terser": "^4.0.4",
"update-section": "^0.3.3",
"vue": "^2.6.3",
"vue-jest": "^3.0.2",
"vue-server-renderer": "^2.6.3",
"vue-template-compiler": "^2.6.3"
},
"files": [
"lib",
"types/index.d.ts",
"types/vue.d.ts"
],
"homepage": "https://github.com/nuxt/vue-meta",
"description": "Manage page meta info in Vue 2.0 server-rendered apps",
"keywords": [
"attribute",
"google",
@@ -82,13 +16,84 @@
"universal",
"vue"
],
"homepage": "https://github.com/nuxt/vue-meta",
"bugs": "https://github.com/nuxt/vue-meta/issues",
"repository": {
"type": "git",
"url": "git@github.com/nuxt/vue-meta.git"
},
"license": "MIT",
"author": "Declan de Wet <declandewet@me.com>",
"files": [
"lib",
"es",
"types/index.d.ts",
"types/vue.d.ts"
],
"main": "lib/vue-meta.common.js",
"web": "lib/vue-meta.js",
"module": "src/index.js",
"module": "es/index.js",
"typings": "types/index.d.ts",
"repository": {
"url": "git@github.com/nuxt/vue-meta.git",
"type": "git"
"scripts": {
"build": "yarn build:other && yarn build:es",
"build:es": "rimraf es && babel src --env-name es --out-dir es --ignore 'src/browser.js'",
"build:other": "rimraf lib && rollup -c scripts/rollup.config.js",
"codecov": "codecov",
"predeploy": "git checkout master && git pull -r",
"deploy": "npm version",
"postdeploy": "git push origin master --follow-tags && npm run release",
"dev": "cd examples && npm run dev && cd ..",
"lint": "eslint src test",
"prerelease": "npm run build",
"release": "npm publish",
"test": "jest",
"toc": "doctoc README.md --title '# Table of Contents'",
"update-cdn": "babel-node scripts/update-cdn.js",
"preversion": "npm run toc",
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\""
},
"dependencies": {
"deepmerge": "^3.2.0",
"lodash.isplainobject": "^4.0.6",
"lodash.uniqueid": "^4.0.1"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.3",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.3.1",
"@nuxtjs/eslint-config": "^0.0.1",
"@vue/server-test-utils": "^1.0.0-beta.29",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
"babel-loader": "^8.0.5",
"codecov": "^3.2.0",
"doctoc": "^1.4.0",
"eslint": "^5.14.1",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.3.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"esm": "^3.2.5",
"jest": "^24.1.0",
"jsdom": "^13.2.0",
"jsdom-global": "^3.0.2",
"rimraf": "^2.6.3",
"rollup": "^1.2.2",
"rollup-plugin-buble": "^0.19.6",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-terser": "^4.0.4",
"update-section": "^0.3.3",
"vue": "^2.6.6",
"vue-jest": "^3.0.3",
"vue-server-renderer": "^2.6.6",
"vue-template-compiler": "^2.6.6"
}
}
-1
View File
@@ -48,7 +48,6 @@ export default [{
output: {
...baseConfig.output,
file: pkg.main,
intro: 'var window',
format: 'cjs'
},
external: Object.keys(pkg.dependencies)
+9 -6
View File
@@ -3,25 +3,28 @@ import createMixin from './shared/mixin'
import setOptions from './shared/options'
import { isUndefined } from './shared/typeof'
import $meta from './client/$meta'
import { hasMetaInfo } from './shared/hasMetaInfo'
/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
function VueMeta(Vue, options = {}) {
function install(Vue, options = {}) {
options = setOptions(options)
Vue.prototype.$meta = $meta(options)
Vue.mixin(createMixin(options))
Vue.mixin(createMixin(Vue, options))
}
VueMeta.version = version
// automatic install
if (!isUndefined(window) && !isUndefined(window.Vue)) {
/* istanbul ignore next */
Vue.use(VueMeta)
install(window.Vue)
}
export default VueMeta
export default {
version,
install,
hasMetaInfo
}
+8 -2
View File
@@ -1,6 +1,10 @@
import { pause, resume } from '../shared/pausing'
import refresh from './refresh'
export default function _$meta(options = {}) {
const _refresh = refresh(options)
const inject = () => {}
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
@@ -8,8 +12,10 @@ export default function _$meta(options = {}) {
*/
return function $meta() {
return {
inject: () => {},
refresh: refresh(options).bind(this)
refresh: _refresh.bind(this),
inject,
pause: pause.bind(this),
resume: resume.bind(this)
}
}
}
+7 -4
View File
@@ -1,8 +1,8 @@
import { isUndefined } from '../shared/typeof'
import { hasGlobalWindow } from '../shared/window'
// fallback to timers if rAF not present
const stopUpdate = (!isUndefined(window) ? window.cancelAnimationFrame : null) || clearTimeout
const startUpdate = (!isUndefined(window) ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
const stopUpdate = (hasGlobalWindow ? window.cancelAnimationFrame : null) || clearTimeout
const startUpdate = (hasGlobalWindow ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
/**
* Performs a batched update. Uses requestAnimationFrame to prevent
@@ -15,7 +15,10 @@ const startUpdate = (!isUndefined(window) ? window.requestAnimationFrame : null)
* @return {Number} id - a new ID
*/
export default function batchUpdate(id, callback) {
stopUpdate(id)
if (id) {
stopUpdate(id)
}
return startUpdate(() => {
id = null
callback()
+10 -3
View File
@@ -3,6 +3,14 @@ import { isFunction } from '../shared/typeof'
import updateClientMetaInfo from './updateClientMetaInfo'
export default function _refresh(options = {}) {
const escapeSequences = [
[/&/g, '\u0026'],
[/</g, '\u003c'],
[/>/g, '\u003e'],
[/"/g, '\u0022'],
[/'/g, '\u0027']
]
/**
* When called, will update the current meta info with new meta info.
* Useful when updating meta info as the result of an asynchronous
@@ -14,15 +22,14 @@ export default function _refresh(options = {}) {
* @return {Object} - new meta info
*/
return function refresh() {
const metaInfo = getMetaInfo(options, this.$root)
const metaInfo = getMetaInfo(options, this.$root, escapeSequences)
const tags = updateClientMetaInfo(options, metaInfo)
// emit "event" with new info
if (tags && isFunction(metaInfo.changed)) {
metaInfo.changed.call(this, metaInfo, tags.addedTags, tags.removedTags)
}
return metaInfo
return { vm: this, metaInfo, tags }
}
}
+14
View File
@@ -0,0 +1,14 @@
import batchUpdate from './batchUpdate'
// store an id to keep track of DOM updates
let batchId = null
export default function triggerUpdate(vm, hookName) {
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
// batch potential DOM updates to prevent extraneous re-rendering
batchId = batchUpdate(batchId, () => {
vm.$meta().refresh()
batchId = null
})
}
}
+8 -5
View File
@@ -2,19 +2,22 @@ import { version } from '../package.json'
import createMixin from './shared/mixin'
import setOptions from './shared/options'
import $meta from './server/$meta'
import { hasMetaInfo } from './shared/hasMetaInfo'
/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
function VueMeta(Vue, options = {}) {
function install(Vue, options = {}) {
options = setOptions(options)
Vue.prototype.$meta = $meta(options)
Vue.mixin(createMixin(options))
Vue.mixin(createMixin(Vue, options))
}
VueMeta.version = version
export default VueMeta
export default {
version,
install,
hasMetaInfo
}
+8 -2
View File
@@ -1,7 +1,11 @@
import refresh from '../client/refresh'
import { pause, resume } from '../shared/pausing'
import inject from './inject'
export default function _$meta(options = {}) {
const _refresh = refresh(options)
const _inject = inject(options)
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
@@ -9,8 +13,10 @@ export default function _$meta(options = {}) {
*/
return function $meta() {
return {
inject: inject(options).bind(this),
refresh: refresh(options).bind(this)
refresh: _refresh.bind(this),
inject: _inject.bind(this),
pause: pause.bind(this),
resume: resume.bind(this)
}
}
}
+9 -2
View File
@@ -3,6 +3,14 @@ import { metaInfoOptionKeys } from '../shared/constants'
import generateServerInjector from './generateServerInjector'
export default function _inject(options = {}) {
const escapeSequences = [
[/&/g, '&amp;'],
[/</g, '&lt;'],
[/>/g, '&gt;'],
[/"/g, '&quot;'],
[/'/g, '&#x27;']
]
/**
* Converts the state of the meta info object such that each item
* can be compiled to a tag string on the server
@@ -10,10 +18,9 @@ export default function _inject(options = {}) {
* @this {Object} - Vue instance - ideally the root component
* @return {Object} - server meta info with `toString` methods
*/
return function inject() {
// get meta info with sensible defaults
const metaInfo = getMetaInfo(options, this.$root)
const metaInfo = getMetaInfo(options, this.$root, escapeSequences)
// generate server injectors
for (const key in metaInfo) {
+19
View File
@@ -0,0 +1,19 @@
import isArray from './isArray'
import { isObject } from './typeof'
export function ensureIsArray(arg, key) {
if (!key || !isObject(arg)) {
return isArray(arg) ? arg : []
}
if (!isArray(arg[key])) {
arg[key] = []
}
return arg
}
export function ensuredPush(object, key, el) {
ensureIsArray(object, key)
object[key].push(el)
}
+1 -1
View File
@@ -1,6 +1,6 @@
import deepmerge from 'deepmerge'
import uniqueId from 'lodash.uniqueid'
import { isUndefined, isFunction, isObject } from '../shared/typeof'
import { isUndefined, isFunction, isObject } from './typeof'
import uniqBy from './uniqBy'
/**
+3 -19
View File
@@ -1,25 +1,9 @@
import deepmerge from 'deepmerge'
import isPlainObject from 'lodash.isplainobject'
import { isUndefined, isFunction, isString } from '../shared/typeof'
import { isFunction, isString } from './typeof'
import isArray from './isArray'
import getComponentOption from './getComponentOption'
const escapeHTML = str => isUndefined(window)
// server-side escape sequence
? String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
// client-side escape sequence
: String(str)
.replace(/&/g, '\u0026')
.replace(/</g, '\u003c')
.replace(/>/g, '\u003e')
.replace(/"/g, '\u0022')
.replace(/'/g, '\u0027')
const applyTemplate = (component, template, chunk) =>
isFunction(template) ? template.call(component, chunk) : template.replace(/%s/g, chunk)
@@ -30,7 +14,7 @@ const applyTemplate = (component, template, chunk) =>
* @param {Object} component - the Vue instance to get meta info from
* @return {Object} - returned meta info
*/
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, component) {
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, component, escapeSequences = []) {
// set some sane defaults
const defaultInfo = {
title: '',
@@ -139,7 +123,7 @@ export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName
if (!isDisabled) {
if (isString(val)) {
escaped[key] = escapeHTML(val)
escaped[key] = escapeSequences.reduce((val, [v, r]) => val.replace(v, r), val)
} else if (isPlainObject(val)) {
escaped[key] = escape(val)
} else if (isArray(val)) {
+3
View File
@@ -0,0 +1,3 @@
export function hasMetaInfo(vm = this) {
return vm && !!vm._vueMeta
}
+63 -83
View File
@@ -1,28 +1,36 @@
import batchUpdate from '../client/batchUpdate'
import { isUndefined, isFunction } from '../shared/typeof'
export default function createMixin(options) {
// store an id to keep track of DOM updates
let batchID = null
import triggerUpdate from '../client/triggerUpdate'
import { isUndefined, isFunction } from './typeof'
import { ensuredPush } from './ensure'
export default function createMixin(Vue, options) {
// for which Vue lifecycle hooks should the metaInfo be refreshed
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
const triggerUpdate = (vm) => {
if (vm.$root._vueMetaInitialized) {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => vm.$meta().refresh())
}
}
// watch for client side component updates
return {
beforeCreate() {
Object.defineProperty(this, '_hasMetaInfo', {
get() {
// Show deprecation warning once when devtools enabled
if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) {
console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please import hasMetaInfo and use hasMetaInfo(vm) instead') // eslint-disable-line no-console
this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true
}
return !!this._vueMeta
}
})
// Add a marker to know if it uses metaInfo
// _vnode is used to know that it's attached to a real component
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) {
this._hasMetaInfo = true
if (!this.$root._vueMeta) {
this.$root._vueMeta = {}
}
if (!this._vueMeta) {
this._vueMeta = true
}
// coerce function-style metaInfo to a computed prop so we can observe
// it on creation
@@ -36,41 +44,62 @@ export default function createMixin(options) {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
this.$options.created = this.$options.created || []
this.$options.created.push(() => {
this.$watch('$metaInfo', () => triggerUpdate(this))
ensuredPush(this.$options, 'created', () => {
this.$watch('$metaInfo', function () {
triggerUpdate(this, 'watcher')
})
})
}
}
updateOnLifecycleHook.forEach((lifecycleHook) => {
this.$options[lifecycleHook] = this.$options[lifecycleHook] || []
this.$options[lifecycleHook].push(() => triggerUpdate(this))
})
// force an initial refresh on page load and prevent other lifecycleHooks
// to triggerUpdate until this initial refresh is finished
// this is to make sure that when a page is opened in an inactive tab which
// has throttled rAF/timers we still immeditately set the page title
if (isUndefined(this.$root._vueMetaInitialized)) {
this.$root._vueMetaInitialized = false
if (isUndefined(this.$root._vueMeta.initialized)) {
this.$root._vueMeta.initialized = this.$isServer
this.$root.$options.mounted = this.$root.$options.mounted || []
this.$root.$options.mounted.push(() => {
if (!this.$root._vueMetaInitialized) {
this.$nextTick(function () {
this.$root.$meta().refresh()
this.$root._vueMetaInitialized = true
if (!this.$root._vueMeta.initialized) {
const $rootMeta = this.$root.$meta()
ensuredPush(this.$options, 'mounted', () => {
if (!this.$root._vueMeta.initialized) {
// refresh meta in nextTick so all child components have loaded
this.$nextTick(function () {
$rootMeta.refresh()
this.$root._vueMeta.initialized = true
})
}
})
// add vue-router navigation guard to prevent multiple updates during navigation
// only usefull on the client side
if (options.refreshOnceOnNavigation && this.$root.$router) {
const $router = this.$root.$router
$router.beforeEach((to, from, next) => {
$rootMeta.pause()
next()
})
$router.afterEach(() => {
const { vm, metaInfo } = $rootMeta.resume()
if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) {
metaInfo.afterNavigation.call(vm, metaInfo)
}
})
}
})
}
}
// do not trigger refresh on the server side
if (!this.$isServer) {
// no need to add this hooks on server side
updateOnLifecycleHook.forEach((lifecycleHook) => {
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
})
// re-render meta data when returning from a child component to parent
this.$options.destroyed = this.$options.destroyed || []
this.$options.destroyed.push(() => {
ensuredPush(this.$options, 'destroyed', () => {
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) {
@@ -83,60 +112,11 @@ export default function createMixin(options) {
return
}
triggerUpdate(this)
triggerUpdate(this, 'destroyed')
}, 50)
})
}
}
}
/* Not yet removed
created() {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
if (!this.$isServer && this.$metaInfo) {
this.$watch('$metaInfo', () => triggerUpdate(this))
}
},
activated() {
if (this._hasMetaInfo) {
triggerUpdate(this)
}
},
deactivated() {
if (this._hasMetaInfo) {
triggerUpdate(this)
}
},
beforeMount() {
if (this._hasMetaInfo) {
triggerUpdate(this)
}
},
destroyed() {
// do not trigger refresh on the server side
if (this.$isServer) {
return
}
// re-render meta data when returning from a child component to parent
if (this._hasMetaInfo) {
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) {
return
}
clearInterval(interval)
if (!this.$parent) {
return
}
triggerUpdate(this)
}, 50)
}
}/**/
}
}
+12 -1
View File
@@ -1,4 +1,5 @@
import { isObject } from '../shared/typeof'
import { isObject, isFunction } from './typeof'
import {
keyName,
attribute,
@@ -28,5 +29,15 @@ export default function setOptions(options) {
}
}
if (options.afterNavigation && !isFunction(options.afterNavigation)) {
console.warn(`afterNavigation should be a function, received ${typeof options.afterNavigation} instead`) // eslint-disable-line no-console
options.afterNavigation = void 0
return options
}
if (options.afterNavigation && !options.refreshOnceOnNavigation) {
options.refreshOnceOnNavigation = true
}
return options
}
+13
View File
@@ -0,0 +1,13 @@
export function pause(refresh = true) {
this.$root._vueMeta.paused = true
return () => resume(refresh)
}
export function resume(refresh = true) {
this.$root._vueMeta.paused = false
if (refresh) {
return this.$root.$meta().refresh()
}
}
-1
View File
@@ -1,4 +1,3 @@
export function isUndefined(arg) {
return typeof arg === 'undefined'
}
+11
View File
@@ -0,0 +1,11 @@
import { isUndefined } from './typeof'
export function hasGlobalWindowFn() {
try {
return !isUndefined(window)
} catch (e) {
return false
}
}
export const hasGlobalWindow = hasGlobalWindowFn()
+37
View File
@@ -0,0 +1,37 @@
import _getMetaInfo from '../src/shared/getMetaInfo'
import { defaultOptions, loadVueMetaPlugin } from './utils'
const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, component, escapeSequences)
describe('escaping', () => {
let Vue
beforeAll(() => (Vue = loadVueMetaPlugin()))
test('special chars are escaped unless disabled', () => {
const component = new Vue({
metaInfo: {
title: 'Hello & Goodbye',
script: [{ innerHTML: 'Hello & Goodbye' }],
__dangerouslyDisableSanitizers: ['script']
}
})
expect(getMetaInfo(component, [[/&/g, '&amp;']])).toEqual({
title: 'Hello &amp; Goodbye',
titleChunk: 'Hello &amp; Goodbye',
titleTemplate: '%s',
htmlAttrs: {},
headAttrs: {},
bodyAttrs: {},
meta: [],
base: [],
link: [],
style: [],
script: [{ innerHTML: 'Hello & Goodbye' }],
noscript: [],
__dangerouslyDisableSanitizers: ['script'],
__dangerouslyDisableSanitizersByTagID: {}
})
})
})
+69 -1
View File
@@ -1,5 +1,9 @@
import { mount, defaultOptions, VueMetaBrowserPlugin, loadVueMetaPlugin } from './utils'
import triggerUpdate from '../src/client/triggerUpdate'
import batchUpdate from '../src/client/batchUpdate'
import { mount, defaultOptions, vmTick, VueMetaBrowserPlugin, loadVueMetaPlugin } from './utils'
jest.mock('../src/client/triggerUpdate')
jest.mock('../src/client/batchUpdate')
jest.mock('../package.json', () => ({
version: 'test-version'
}))
@@ -7,6 +11,7 @@ jest.mock('../package.json', () => ({
describe('plugin', () => {
let Vue
beforeEach(() => jest.clearAllMocks())
beforeAll(() => (Vue = loadVueMetaPlugin(true)))
test('is loaded', () => {
@@ -35,4 +40,67 @@ describe('plugin', () => {
test('plugin sets package version', () => {
expect(VueMetaBrowserPlugin.version).toBe('test-version')
})
test('updates can be paused and resumed', async () => {
const _triggerUpdate = jest.requireActual('../src/client/triggerUpdate').default
const _batchUpdate = jest.requireActual('../src/client/batchUpdate').default
const triggerUpdateSpy = triggerUpdate.mockImplementation(_triggerUpdate)
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
const Component = Vue.component('test-component', {
metaInfo() {
return {
title: this.title
}
},
props: {
title: {
type: String,
default: ''
}
},
template: '<div>Test</div>'
})
let title = 'first title'
const wrapper = mount(Component, {
localVue: Vue,
propsData: {
title
}
})
// no batchUpdate on initialization
expect(wrapper.vm.$root._vueMeta.initialized).toBe(false)
expect(wrapper.vm.$root._vueMeta.paused).toBeFalsy()
expect(triggerUpdateSpy).toHaveBeenCalledTimes(1)
expect(batchUpdateSpy).not.toHaveBeenCalled()
jest.clearAllMocks()
await vmTick(wrapper.vm)
title = 'second title'
wrapper.setProps({ title })
// batchUpdate on normal update
expect(wrapper.vm.$root._vueMeta.initialized).toBe(true)
expect(wrapper.vm.$root._vueMeta.paused).toBeFalsy()
expect(triggerUpdateSpy).toHaveBeenCalledTimes(1)
expect(batchUpdateSpy).toHaveBeenCalledTimes(1)
jest.clearAllMocks()
wrapper.vm.$meta().pause()
title = 'third title'
wrapper.setProps({ title })
// no batchUpdate when paused
expect(wrapper.vm.$root._vueMeta.initialized).toBe(true)
expect(wrapper.vm.$root._vueMeta.paused).toBe(true)
expect(triggerUpdateSpy).toHaveBeenCalledTimes(1)
expect(batchUpdateSpy).not.toHaveBeenCalled()
jest.clearAllMocks()
const { metaInfo } = wrapper.vm.$meta().resume()
expect(metaInfo.title).toBe(title)
})
})
+40
View File
@@ -7,6 +7,7 @@ jest.mock('../package.json', () => ({
describe('plugin', () => {
let Vue
beforeEach(() => jest.clearAllMocks())
beforeAll(() => (Vue = loadVueMetaPlugin()))
test('is loaded', () => {
@@ -29,4 +30,43 @@ describe('plugin', () => {
test('plugin sets package version', () => {
expect(VueMetaServerPlugin.version).toBe('test-version')
})
test('prints deprecation warning once when using _hasMetaInfo', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
const Component = Vue.component('test-component', {
template: '<div>Test</div>',
[defaultOptions.keyName]: {
title: 'Hello World'
}
})
Vue.config.devtools = true
const { vm } = mount(Component, { localVue: Vue })
expect(vm._hasMetaInfo).toBe(true)
expect(warn).toHaveBeenCalledTimes(1)
expect(vm._hasMetaInfo).toBe(true)
expect(warn).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
test('can use hasMetaInfo export', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
const Component = Vue.component('test-component', {
template: '<div>Test</div>',
[defaultOptions.keyName]: {
title: 'Hello World'
}
})
const { vm } = mount(Component, { localVue: Vue })
expect(VueMetaServerPlugin.hasMetaInfo(vm)).toBe(true)
expect(warn).not.toHaveBeenCalled()
warn.mockRestore()
})
})
+6
View File
@@ -41,3 +41,9 @@ export function loadVueMetaPlugin(browser, options, localVue = getVue()) {
return localVue
}
export const vmTick = (vm) => {
return new Promise((resolve) => {
vm.$nextTick(resolve)
})
}
+2
View File
@@ -46,4 +46,6 @@ export interface MetaInfo {
__dangerouslyDisableSanitizersByTagID?: string[]
changed?: <T extends object>(newInfo: T, addedTags: HTMLElement[], removedTags: HTMLElement[]) => void
afterNavigation?: <T extends object>(vm: Vue, newInfo: T) => void
refreshOnceOnNavigation?: boolean
}
+1613 -3470
View File
File diff suppressed because it is too large Load Diff