diff --git a/README.md b/README.md index 91450dee..9ce21952 100644 --- a/README.md +++ b/README.md @@ -681,38 +681,28 @@ Easy. Instead of defining `metaInfo` as an object, define it as a function and a ## How do I populate `metaInfo` from the result of an asynchronous action? -`vue-meta` exposes a method called `refresh` on the client-side that allows you to trigger an update at any given point in time. +`vue-meta` will do this for you automatically when your component state changes. -In the same way you access `$meta().inject()` on the server, you can access `$meta().refresh()`. - -For example, if you're using Vuex and you have an action that fetches a `post` asynchronously, you should ensure that it returns a promise so that you are notified when the fetching is complete: +Just make sure that you're using the function form of `metaInfo`: ```js { - actions: { - async fetchPost ({ commit }, payload) { - const post = yield db.fetch('posts', payload.postId) - commit('fetchedPost', post) + data () { + return { + title: 'Foo Bar Baz' } } -} -``` - -Then in your component, you can call `refresh()` to trigger an update once the fetch is complete: - -```js -{ - beforeMount () { - const postId = this.$router.params.id - this.$store.dispatch('fetchPost', { postId }) - .then(() => this.$meta().refresh()) + metaInfo () { + return { + title: this.title + } } } ``` -Just make sure that whatever data source you're using (`store` if you're using Vuex, component `data` otherwise) has some sane defaults set so Vue doesn't complain about `null` property accessors. +Check out the [vuex-async](https://github.com/declandewet/vue-meta/tree/master/examples/vuex-async) example for a far more detailed demonstration if you have doubts. -Check out the [vuex-async](https://github.com/declandewet/vue-meta/tree/master/examples/vuex-async) example for a far more detailed demonstration of how this works. +Credit & Thanks for this feature goes to [Sébastien Chopin](https://github.com/Atinux). # Examples diff --git a/examples/vuex-async/store.js b/examples/vuex-async/store.js index 6bbc37bc..9eb0419b 100644 --- a/examples/vuex-async/store.js +++ b/examples/vuex-async/store.js @@ -63,15 +63,10 @@ export default new Vuex.Store({ actions: { getPost ({ commit }, payload) { commit('loadingState', { isLoading: true }) - // we have to return a promise from this action so we know - // when it is finished - return new Promise((resolve) => { - setTimeout(() => { - commit('getPost', payload) - resolve() - }, 2000) - }) - .then(() => commit('loadingState', { isLoading: false })) + setTimeout(() => { + commit('getPost', payload) + commit('loadingState', { isLoading: false }) + }, 2000) } } }) diff --git a/examples/vuex-async/views/Post.vue b/examples/vuex-async/views/Post.vue index 5032983f..15cd5cda 100644 --- a/examples/vuex-async/views/Post.vue +++ b/examples/vuex-async/views/Post.vue @@ -18,11 +18,7 @@ name: 'post', beforeMount () { const { slug } = this.$route.params - // since fetching a post is asynchronous, - // we need to call `this.$meta().refresh()` - // to update the meta info this.$store.dispatch('getPost', { slug }) - .then(() => this.$meta().refresh()) }, computed: mapGetters([ 'isLoading', diff --git a/src/client/batchUpdate.js b/src/client/batchUpdate.js new file mode 100644 index 00000000..f3525b0a --- /dev/null +++ b/src/client/batchUpdate.js @@ -0,0 +1,17 @@ +/** + * Performs a batched update. Uses requestAnimationFrame to prevent + * calling a function too many times in quick succession. + * You need to pass it an ID (which can initially be `null`), + * but be sure to overwrite that ID with the return value of batchUpdate. + * + * @param {(null|Number)} id - the ID of this update + * @param {Function} callback - the update to perform + * @return {Number} id - a new ID + */ +export default function batchUpdate (id, callback) { + window.cancelAnimationFrame(id) + return window.requestAnimationFrame(() => { + id = null + callback() + }) +} diff --git a/src/shared/plugin.js b/src/shared/plugin.js index 6f975128..529a97d7 100644 --- a/src/shared/plugin.js +++ b/src/shared/plugin.js @@ -1,5 +1,6 @@ import assign from 'object-assign' import $meta from './$meta' +import batchUpdate from '../client/batchUpdate' import { VUE_META_KEY_NAME, @@ -33,18 +34,31 @@ export default function VueMeta (Vue, options = {}) { Vue.prototype.$meta = $meta(options) // store an id to keep track of DOM updates - let requestId = null + let batchID = null // watch for client side component updates Vue.mixin({ + beforeCreate () { + // coerce function-style metaInfo to a computed prop so we can observe + // it on creation + if (typeof this.$options[options.keyName] === 'function') { + this.$options.computed.$metaInfo = this.$options[options.keyName] + } + }, + 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.$metaInfo) { + this.$watch('$metaInfo', () => { + // batch potential DOM updates to prevent extraneous re-rendering + batchID = batchUpdate(batchID, () => this.$meta().refresh()) + }) + } + }, beforeMount () { // batch potential DOM updates to prevent extraneous re-rendering - window.cancelAnimationFrame(requestId) - - requestId = window.requestAnimationFrame(() => { - requestId = null - this.$meta().refresh() - }) + batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }) }