From 8e211751df2696a7777b66a60b15b6d87f02f81e Mon Sep 17 00:00:00 2001 From: pimlie Date: Wed, 20 Feb 2019 18:35:01 +0100 Subject: [PATCH] feat: add option to refresh once during navigation (possible fix for #320) chore: add es build chore: global window detection chore: small refactor improvements --- README.md | 6 ++++++ scripts/build.sh | 17 +++++++++++++++ src/client/$meta.js | 7 ++++-- src/client/triggerUpdate.js | 14 ++++++++++++ src/server/$meta.js | 7 ++++-- src/shared/ensure.js | 19 ++++++++++++++++ src/shared/getComponentOption.js | 2 +- src/shared/mixin.js | 16 +++++++++++++- src/shared/options.js | 3 ++- src/shared/pausing.js | 13 +++++++++++ src/shared/window.js | 11 ++++++++++ test/escaping.test.js | 37 ++++++++++++++++++++++++++++++++ 12 files changed, 145 insertions(+), 7 deletions(-) create mode 100755 scripts/build.sh create mode 100644 src/client/triggerUpdate.js create mode 100644 src/shared/ensure.js create mode 100644 src/shared/pausing.js create mode 100644 src/shared/window.js create mode 100644 test/escaping.test.js diff --git a/README.md b/README.md index 7e5e1dfa..6b795757 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ - [`__dangerouslyDisableSanitizers` ([String])](#__dangerouslydisablesanitizers-string) - [`__dangerouslyDisableSanitizersByTagID` ({[String]})](#__dangerouslydisablesanitizersbytagid-string) - [`changed` (Function)](#changed-function) + - [`refreshOnceOnNavigation` (Boolean)](#refreshonceonnavigation-boolean) - [How `metaInfo` is Resolved](#how-metainfo-is-resolved) - [Lists of Tags](#lists-of-tags) - [Performance](#performance) @@ -652,6 +653,11 @@ 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 with vue-router and only refresh once when navigation has finished. It does this by adding a global beforeEach and afterEach navigation guard on the $router instance. + + ### 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: diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 00000000..7af8d159 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -e + +# Cleanup +rm -rf lib es + +echo 'Compile JS...' +rollup -c scripts/rollup.config.js +echo 'Done.' +echo '' + +echo 'Build ES modules...' +NODE_ENV=es babel src --out-dir es --ignore 'src/browser.js' +echo 'Done.' +echo '' + +echo 'Done building assets.' diff --git a/src/client/$meta.js b/src/client/$meta.js index 79865d13..c9c7c47b 100644 --- a/src/client/$meta.js +++ b/src/client/$meta.js @@ -2,6 +2,9 @@ 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) @@ -9,8 +12,8 @@ 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) } diff --git a/src/client/triggerUpdate.js b/src/client/triggerUpdate.js new file mode 100644 index 00000000..19105bb2 --- /dev/null +++ b/src/client/triggerUpdate.js @@ -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._vueMetaInitialized && !vm.$root._vueMetaPaused) { + // batch potential DOM updates to prevent extraneous re-rendering + batchId = batchUpdate(batchId, () => { + vm.$meta().refresh() + batchId = null + }) + } +} diff --git a/src/server/$meta.js b/src/server/$meta.js index 4bb35826..cd700b1e 100644 --- a/src/server/$meta.js +++ b/src/server/$meta.js @@ -3,6 +3,9 @@ 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) @@ -10,8 +13,8 @@ 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) } diff --git a/src/shared/ensure.js b/src/shared/ensure.js new file mode 100644 index 00000000..a01653e9 --- /dev/null +++ b/src/shared/ensure.js @@ -0,0 +1,19 @@ +import isArray from './isArray' +import { isObject } from './typeof' + +export function ensureIsArray(arg, key) { + if (key && isObject(arg)) { + if (!isArray(arg[key])) { + arg[key] = [] + } + return arg + } else { + return isArray(arg) ? arg : [] + } +} + +export function ensuredPush(object, key, el) { + ensureIsArray(object, key) + + object[key].push(el) +} diff --git a/src/shared/getComponentOption.js b/src/shared/getComponentOption.js index 9fa7b112..f4685204 100644 --- a/src/shared/getComponentOption.js +++ b/src/shared/getComponentOption.js @@ -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' /** diff --git a/src/shared/mixin.js b/src/shared/mixin.js index 59225a99..50ee1a81 100644 --- a/src/shared/mixin.js +++ b/src/shared/mixin.js @@ -43,15 +43,29 @@ export default function createMixin(options) { this.$root._vueMetaInitialized = this.$isServer if (!this.$root._vueMetaInitialized) { + const $rootMeta = this.$root.$meta() + ensuredPush(this.$options, 'mounted', () => { if (!this.$root._vueMetaInitialized) { // refresh meta in nextTick so all child components have loaded this.$nextTick(function () { - this.$root.$meta().refresh() + $rootMeta.refresh() this.$root._vueMetaInitialized = 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(() => $rootMeta.resume()) + } } } diff --git a/src/shared/options.js b/src/shared/options.js index d20805df..65f97f07 100644 --- a/src/shared/options.js +++ b/src/shared/options.js @@ -1,4 +1,5 @@ -import { isObject } from '../shared/typeof' +import { isObject } from './typeof' + import { keyName, attribute, diff --git a/src/shared/pausing.js b/src/shared/pausing.js new file mode 100644 index 00000000..afd7dfe5 --- /dev/null +++ b/src/shared/pausing.js @@ -0,0 +1,13 @@ +export function pause(refresh = true) { + this.$root._vueMetaPaused = true + + return () => resume(refresh) +} + +export function resume(refresh = true) { + this.$root._vueMetaPaused = false + + if (refresh) { + return this.$root.$meta().refresh() + } +} diff --git a/src/shared/window.js b/src/shared/window.js new file mode 100644 index 00000000..118705e9 --- /dev/null +++ b/src/shared/window.js @@ -0,0 +1,11 @@ +import { isUndefined } from './typeof' + +export function hasGlobalWindowFn() { + try { + return !isUndefined(window) + } catch (e) { + return false + } +} + +export const hasGlobalWindow = hasGlobalWindowFn() diff --git a/test/escaping.test.js b/test/escaping.test.js new file mode 100644 index 00000000..347813e5 --- /dev/null +++ b/test/escaping.test.js @@ -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, '&']])).toEqual({ + title: 'Hello & Goodbye', + titleChunk: 'Hello & Goodbye', + titleTemplate: '%s', + htmlAttrs: {}, + headAttrs: {}, + bodyAttrs: {}, + meta: [], + base: [], + link: [], + style: [], + script: [{ innerHTML: 'Hello & Goodbye' }], + noscript: [], + __dangerouslyDisableSanitizers: ['script'], + __dangerouslyDisableSanitizersByTagID: {} + }) + }) +})