diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js index fad93e5..04c184d 100644 --- a/scripts/rollup.config.js +++ b/scripts/rollup.config.js @@ -48,8 +48,8 @@ export default [{ output: { ...baseConfig.output, file: pkg.main, - intro: 'var window', - format: 'cjs' + format: 'cjs', + intro: 'var window' }, external: Object.keys(pkg.dependencies) }] diff --git a/src/client/$meta.js b/src/client/$meta.js index a3fd39a..79865d1 100644 --- a/src/client/$meta.js +++ b/src/client/$meta.js @@ -1,3 +1,4 @@ +import { pause, resume } from '../shared/pausing' import refresh from './refresh' export default function _$meta(options = {}) { @@ -9,7 +10,9 @@ export default function _$meta(options = {}) { return function $meta() { return { inject: () => {}, - refresh: refresh(options).bind(this) + refresh: refresh(options).bind(this), + pause: pause.bind(this), + resume: resume.bind(this) } } } diff --git a/src/client/batchUpdate.js b/src/client/batchUpdate.js index eb54aa7..765a92b 100644 --- a/src/client/batchUpdate.js +++ b/src/client/batchUpdate.js @@ -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() diff --git a/src/client/refresh.js b/src/client/refresh.js index 1ef721f..c6b5354 100644 --- a/src/client/refresh.js +++ b/src/client/refresh.js @@ -17,7 +17,6 @@ export default function _refresh(options = {}) { const metaInfo = getMetaInfo(options, this.$root) const tags = updateClientMetaInfo(options, metaInfo) - // emit "event" with new info if (tags && isFunction(metaInfo.changed)) { metaInfo.changed.call(this, metaInfo, tags.addedTags, tags.removedTags) diff --git a/src/server/$meta.js b/src/server/$meta.js index ee40163..4bb3582 100644 --- a/src/server/$meta.js +++ b/src/server/$meta.js @@ -1,4 +1,5 @@ import refresh from '../client/refresh' +import { pause, resume } from '../shared/pausing' import inject from './inject' export default function _$meta(options = {}) { @@ -10,7 +11,9 @@ export default function _$meta(options = {}) { return function $meta() { return { inject: inject(options).bind(this), - refresh: refresh(options).bind(this) + refresh: refresh(options).bind(this), + pause: pause.bind(this), + resume: resume.bind(this) } } } diff --git a/src/shared/mixin.js b/src/shared/mixin.js index 89b2b74..4976ff0 100644 --- a/src/shared/mixin.js +++ b/src/shared/mixin.js @@ -1,20 +1,11 @@ -import batchUpdate from '../client/batchUpdate' +import triggerUpdate from '../client/triggerUpdate' import { isUndefined, isFunction } from '../shared/typeof' +import { ensuredPush } from '../shared/ensure' export default function createMixin(options) { - // store an id to keep track of DOM updates - let batchID = null - // 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() { @@ -36,41 +27,41 @@ 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)) + ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook)) }) // 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._vueMetaPaused)) { + this.$root._vueMetaInitialized = 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._vueMetaInitialized) { + ensuredPush(this.$options, 'mounted', () => { + if (!this.$root._vueMetaInitialized) { + this.$nextTick(function () { + this.$root.$meta().refresh() + this.$root._vueMetaInitialized = true + }) + } + }) + } } // do not trigger refresh on the server side if (!this.$isServer) { // 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,7 +74,7 @@ export default function createMixin(options) { return } - triggerUpdate(this) + triggerUpdate(this, 'destroyed') }, 50) }) } diff --git a/test/plugin-browser.test.js b/test/plugin-browser.test.js index 93d83ce..59f84c9 100644 --- a/test/plugin-browser.test.js +++ b/test/plugin-browser.test.js @@ -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,64 @@ 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 triggerUpdateSpy = triggerUpdate.mockImplementation(_triggerUpdate) + + const Component = Vue.component('test-component', { + metaInfo() { + return { + title: this.title + } + }, + props: { + title: { + type: String, + default: '' + } + }, + template: '
Test
' + }) + + let title = 'first title' + const wrapper = mount(Component, { + localVue: Vue, + propsData: { + title + } + }) + + // no batchUpdate on initialization + expect(wrapper.vm.$root._vueMetaInitialized).toBe(false) + expect(wrapper.vm.$root._vueMetaPaused).toBeFalsy() + expect(triggerUpdateSpy).toHaveBeenCalledTimes(1) + expect(batchUpdate).not.toHaveBeenCalled() + jest.clearAllMocks() + await vmTick(wrapper.vm) + + title = 'second title' + wrapper.setProps({ title }) + + // batchUpdate on normal update + expect(wrapper.vm.$root._vueMetaInitialized).toBe(true) + expect(wrapper.vm.$root._vueMetaPaused).toBeFalsy() + expect(triggerUpdateSpy).toHaveBeenCalledTimes(1) + expect(batchUpdate).toHaveBeenCalledTimes(1) + jest.clearAllMocks() + + wrapper.vm.$meta().pause() + title = 'third title' + wrapper.setProps({ title }) + + // no batchUpdate when paused + expect(wrapper.vm.$root._vueMetaInitialized).toBe(true) + expect(wrapper.vm.$root._vueMetaPaused).toBe(true) + expect(triggerUpdateSpy).toHaveBeenCalledTimes(1) + expect(batchUpdate).not.toHaveBeenCalled() + jest.clearAllMocks() + + const metaInfo = wrapper.vm.$meta().resume() + expect(metaInfo.title).toBe(title) + }) }) diff --git a/test/utils/index.js b/test/utils/index.js index 318b422..fee4c7b 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -41,3 +41,9 @@ export function loadVueMetaPlugin(browser, options, localVue = getVue()) { return localVue } + +export const vmTick = (vm) => { + return new Promise((resolve) => { + vm.$nextTick(resolve) + }) +}