From 34c6ad957d9361bd44ca11a45a0560940cda9629 Mon Sep 17 00:00:00 2001 From: Pim Date: Thu, 6 Jun 2019 10:01:11 +0200 Subject: [PATCH] fix: detect and apply changes triggered before or during initialization (#377) --- src/client/update.js | 7 ++++ src/shared/mixin.js | 19 ++++++++- test/unit/components.test.js | 76 ++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/client/update.js b/src/client/update.js index c03a88e..e56d6f6 100644 --- a/src/client/update.js +++ b/src/client/update.js @@ -2,6 +2,13 @@ let batchId = null export function triggerUpdate(vm, hookName) { + // if an update was triggered during initialization or when an update was triggered by the + // metaInfo watcher, set initialized to null + // then we keep falsy value but know we need to run a triggerUpdate after initialization + if (!vm.$root._vueMeta.initialized && (vm.$root._vueMeta.initializing || hookName === 'watcher')) { + vm.$root._vueMeta.initialized = null + } + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { // batch potential DOM updates to prevent extraneous re-rendering batchUpdate(() => vm.$meta().refresh()) diff --git a/src/shared/mixin.js b/src/shared/mixin.js index 25c4d0a..f9a43a9 100644 --- a/src/shared/mixin.js +++ b/src/shared/mixin.js @@ -72,12 +72,29 @@ export default function createMixin(Vue, options) { this.$root._vueMeta.initialized = this.$isServer if (!this.$root._vueMeta.initialized) { + // we use the mounted hook here as on page load ensuredPush(this.$options, 'mounted', () => { if (!this.$root._vueMeta.initialized) { + // used in triggerUpdate to check if a change was triggered + // during initialization + this.$root._vueMeta.initializing = true + // refresh meta in nextTick so all child components have loaded this.$nextTick(function () { - const { metaInfo } = this.$root.$meta().refresh() + const { tags, metaInfo } = this.$root.$meta().refresh() + + // After ssr hydration (identifier by tags === false) check + // if initialized was set to null in triggerUpdate. That'd mean + // that during initilazation changes where triggered which need + // to be applied OR a metaInfo watcher was triggered before the + // current hook was called + // (during initialization all changes are blocked) + if (tags === false && this.$root._vueMeta.initialized === null) { + this.$nextTick(() => triggerUpdate(this, 'initializing')) + } + this.$root._vueMeta.initialized = true + delete this.$root._vueMeta.initializing // add the navigation guards if they havent been added yet // they are needed for the afterNavigation callback diff --git a/test/unit/components.test.js b/test/unit/components.test.js index 0ead61b..43f857a 100644 --- a/test/unit/components.test.js +++ b/test/unit/components.test.js @@ -197,4 +197,80 @@ describe('client', () => { guards.after() expect(afterNavigation).toHaveBeenCalled() }) + + test('changes before hydration initialization trigger an update', async () => { + html.setAttribute(defaultOptions.ssrAttribute, 'true') + + // this component uses a computed prop to simulate a non-synchronous + // metaInfo update like you would have with a Vuex mutation + const component = Vue.component('test-component', { + data() { + return { + hiddenTheme: 'light' + } + }, + computed: { + theme() { + return this.hiddenTheme + } + }, + beforeMount() { + this.hiddenTheme = 'dark' + }, + render: h => h('div'), + metaInfo() { + return { + htmlAttrs: { + theme: this.theme + } + } + } + }) + + const wrapper = mount(component, { localVue: Vue }) + expect(html.getAttribute('theme')).not.toBe('dark') + + await vmTick(wrapper.vm) + jest.runAllTimers() + + expect(html.getAttribute('theme')).toBe('dark') + html.removeAttribute('theme') + }) + + test('changes during hydration initialization trigger an update', async () => { + html.setAttribute(defaultOptions.ssrAttribute, 'true') + + const component = Vue.component('test-component', { + data() { + return { + hiddenTheme: 'light' + } + }, + computed: { + theme() { + return this.hiddenTheme + } + }, + mounted() { + this.hiddenTheme = 'dark' + }, + render: h => h('div'), + metaInfo() { + return { + htmlAttrs: { + theme: this.theme + } + } + } + }) + + const wrapper = mount(component, { localVue: Vue }) + expect(html.getAttribute('theme')).not.toBe('dark') + + await vmTick(wrapper.vm) + jest.runAllTimers() + + expect(html.getAttribute('theme')).toBe('dark') + html.removeAttribute('theme') + }) })