mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-22 12:10:34 +03:00
More reliable strategy for getting deepmost component + addition of refresh() method + example & documentation on asynchronous updates
This commit is contained in:
@@ -74,6 +74,7 @@
|
|||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [How do I use component data in `metaInfo`?](#how-do-i-use-component-data-in-metainfo)
|
- [How do I use component data in `metaInfo`?](#how-do-i-use-component-data-in-metainfo)
|
||||||
- [How do I use component props in `metaInfo`?](#how-do-i-use-component-props-in-metainfo)
|
- [How do I use component props in `metaInfo`?](#how-do-i-use-component-props-in-metainfo)
|
||||||
|
- [How do I populate `metaInfo` from the result of an asynchronous action?](#how-do-i-populate-metainfo-from-the-result-of-an-asynchronous-action)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
@@ -675,3 +676,38 @@ The same way you use data - specify a function instead of an object. It will nee
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
async fetchPost ({ commit }, payload) {
|
||||||
|
const post = yield db.fetch('posts', payload.postId)
|
||||||
|
commit('fetchedPost', post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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 of how this works.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<li><a href="basic-render">Basic Render</a></li>
|
<li><a href="basic-render">Basic Render</a></li>
|
||||||
<li><a href="vue-router">Usage with vue-router</a></li>
|
<li><a href="vue-router">Usage with vue-router</a></li>
|
||||||
<li><a href="vuex">Usage with vuex</a></li>
|
<li><a href="vuex">Usage with vuex</a></li>
|
||||||
|
<li><a href="vuex-async">Usage with vuex + async actions</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<h1>vuex-async</h1>
|
||||||
|
<router-view></router-view>
|
||||||
|
<p>Inspect Element to see the meta info</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import assign from 'object-assign'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import store from './store'
|
||||||
|
import router from './router'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = new Vue(assign(App, { router, store }))
|
||||||
|
|
||||||
|
app.$mount('#app')
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="stylesheet" href="/global.css">
|
||||||
|
<a href="/">← Examples index</a>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/__build__/vuex-async.js"></script>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Router from 'vue-router'
|
||||||
|
import Meta from 'vue-meta'
|
||||||
|
import Home from './views/Home.vue'
|
||||||
|
import Post from './views/Post.vue'
|
||||||
|
|
||||||
|
Vue.use(Router)
|
||||||
|
Vue.use(Meta)
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
|
base: '/vuex-async',
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: Home },
|
||||||
|
{ path: '/posts/:slug', component: Post }
|
||||||
|
]
|
||||||
|
})
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
// STATE
|
||||||
|
state: {
|
||||||
|
isLoading: false,
|
||||||
|
// its important that we set some defaults for the current post
|
||||||
|
// otherwise Vue will complain that properties are `null`
|
||||||
|
post: {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
slug: '',
|
||||||
|
published: false
|
||||||
|
},
|
||||||
|
posts: [{
|
||||||
|
slug: 'a-sample-blog-post',
|
||||||
|
title: 'A Sample Blog Post',
|
||||||
|
content: 'This is the blog post content',
|
||||||
|
published: true
|
||||||
|
}, {
|
||||||
|
slug: 'an-unpublished-blog-post',
|
||||||
|
title: 'An Unpublished Blog Post',
|
||||||
|
content: 'This is the blog post content',
|
||||||
|
published: false
|
||||||
|
}, {
|
||||||
|
slug: 'another-blog-post',
|
||||||
|
title: 'Another Blog Post',
|
||||||
|
content: 'This is the blog post content',
|
||||||
|
published: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
// GETTERS
|
||||||
|
getters: {
|
||||||
|
isLoading (state) {
|
||||||
|
return state.isLoading
|
||||||
|
},
|
||||||
|
post (state) {
|
||||||
|
return state.post
|
||||||
|
},
|
||||||
|
publishedPosts (state) {
|
||||||
|
return state.posts.filter((post) => post.published)
|
||||||
|
},
|
||||||
|
publishedPostsCount (state, getters) {
|
||||||
|
return getters.publishedPosts.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// MUTATIONS
|
||||||
|
mutations: {
|
||||||
|
loadingState (state, { isLoading }) {
|
||||||
|
state.isLoading = isLoading
|
||||||
|
},
|
||||||
|
getPost (state, { slug }) {
|
||||||
|
state.post = state.posts.find((post) => post.slug === slug)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ACTIONS
|
||||||
|
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 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>This is the homepage</h3>
|
||||||
|
<h4>There are <u>{{ postsCount }}</u> published posts</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="post in posts">
|
||||||
|
<router-link :to="'posts/' + post.slug">{{ post.title }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'home',
|
||||||
|
computed: mapGetters({
|
||||||
|
posts: 'publishedPosts',
|
||||||
|
postsCount: 'publishedPostsCount'
|
||||||
|
}),
|
||||||
|
metaInfo: {
|
||||||
|
title: 'Home'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<h3>Loading...</h3>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h3>{{ post.title }}</h3>
|
||||||
|
<p>{{ post.content}}<p>
|
||||||
|
<router-link to="/">Go back home</router-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
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',
|
||||||
|
'post'
|
||||||
|
]),
|
||||||
|
metaInfo: {
|
||||||
|
title () {
|
||||||
|
return this.isLoading ? 'Loading...' : this.post.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,7 +6,15 @@ Vue.use(Vuex)
|
|||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
// STATE
|
// STATE
|
||||||
state: {
|
state: {
|
||||||
post: null,
|
isLoading: false,
|
||||||
|
// its important that we set some defaults for the current post
|
||||||
|
// otherwise Vue will complain that properties are `null`
|
||||||
|
post: {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
slug: '',
|
||||||
|
published: false
|
||||||
|
},
|
||||||
posts: [{
|
posts: [{
|
||||||
slug: 'a-sample-blog-post',
|
slug: 'a-sample-blog-post',
|
||||||
title: 'A Sample Blog Post',
|
title: 'A Sample Blog Post',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'blog-post',
|
name: 'post',
|
||||||
beforeMount () {
|
beforeMount () {
|
||||||
const { slug } = this.$route.params
|
const { slug } = this.$route.params
|
||||||
this.$store.dispatch('getPost', { slug })
|
this.$store.dispatch('getPost', { slug })
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import getMetaInfo from '../shared/getMetaInfo'
|
||||||
|
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When called, will update the current meta info with new meta info.
|
||||||
|
* Useful when updating meta info as the result of an asynchronous
|
||||||
|
* action that resolves after the initial render takes place.
|
||||||
|
*
|
||||||
|
* Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion
|
||||||
|
* to implement this method.
|
||||||
|
*
|
||||||
|
* @return {Object} - new meta info
|
||||||
|
*/
|
||||||
|
export default function refresh () {
|
||||||
|
const info = getMetaInfo(this.$root)
|
||||||
|
updateClientMetaInfo(info)
|
||||||
|
return info
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import inject from './inject'
|
import inject from '../server/inject'
|
||||||
|
import refresh from '../client/refresh'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an injector for server-side rendering.
|
* Returns an injector for server-side rendering.
|
||||||
@@ -7,5 +8,8 @@ import inject from './inject'
|
|||||||
*/
|
*/
|
||||||
export default function $meta () {
|
export default function $meta () {
|
||||||
// bind inject method to this component
|
// bind inject method to this component
|
||||||
return { inject: inject.bind(this) }
|
return {
|
||||||
|
inject: inject.bind(this),
|
||||||
|
refresh: refresh.bind(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,11 @@ import deepmerge from 'deepmerge'
|
|||||||
* @param {Boolean} opts.deep - look for data in child components as well?
|
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||||
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
||||||
* @param {Object} [result={}] - result so far
|
* @param {Object} [result={}] - result so far
|
||||||
* @return {Object} - final aggregated result
|
* @return {Object} result - final aggregated result
|
||||||
|
* @return {Object} result.mergedOption - the actual merged options
|
||||||
|
* @return {Object} result.deepestComponentWithMetaInfo - the deepest component in the heirarchy that has a `metaInfo` instance property
|
||||||
*/
|
*/
|
||||||
export default function getComponentOption (opts, result = {}) {
|
export default function getComponentOption (opts, result = { mergedOption: {} }) {
|
||||||
const { component, option, deep, arrayMerge } = opts
|
const { component, option, deep, arrayMerge } = opts
|
||||||
const { $options } = component
|
const { $options } = component
|
||||||
|
|
||||||
@@ -24,12 +26,13 @@ export default function getComponentOption (opts, result = {}) {
|
|||||||
|
|
||||||
if (typeof data === 'object') {
|
if (typeof data === 'object') {
|
||||||
// merge with existing options
|
// merge with existing options
|
||||||
result = deepmerge(result, data, {
|
result.mergedOption = deepmerge(result.mergedOption, data, {
|
||||||
clone: true,
|
clone: true,
|
||||||
arrayMerge
|
arrayMerge
|
||||||
})
|
})
|
||||||
|
result.deepestComponentWithMetaInfo = component
|
||||||
} else {
|
} else {
|
||||||
result = data
|
result.mergedOption = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import deepmerge from 'deepmerge'
|
import deepmerge from 'deepmerge'
|
||||||
import getComponentOption from './getComponentOption'
|
import getComponentOption from './getComponentOption'
|
||||||
import mergeComponentData from './mergeComponentData'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the correct meta info for the given component
|
* Returns the correct meta info for the given component
|
||||||
@@ -26,7 +25,7 @@ export default function getMetaInfo (component) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collect & aggregate all metaInfo $options
|
// collect & aggregate all metaInfo $options
|
||||||
const info = getComponentOption({
|
const { mergedOption: info, deepestComponentWithMetaInfo } = getComponentOption({
|
||||||
component,
|
component,
|
||||||
option: 'metaInfo',
|
option: 'metaInfo',
|
||||||
deep: true,
|
deep: true,
|
||||||
@@ -73,13 +72,12 @@ export default function getMetaInfo (component) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metaInfo = deepmerge(defaultInfo, info)
|
const metaInfo = deepmerge(defaultInfo, info)
|
||||||
const componentData = mergeComponentData(component)
|
|
||||||
|
|
||||||
// inject component context into functions & call to normalize data
|
// inject component context into functions & call to normalize data
|
||||||
Object.keys(metaInfo).forEach((key) => {
|
Object.keys(metaInfo).forEach((key) => {
|
||||||
const val = metaInfo[key]
|
const val = metaInfo[key]
|
||||||
if (typeof val === 'function') {
|
if (typeof val === 'function') {
|
||||||
metaInfo[key] = val.call(componentData)
|
metaInfo[key] = val.call(deepestComponentWithMetaInfo)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import assign from 'object-assign'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively shallow-merges component object with it's children component objects.
|
|
||||||
* This function is responsible for obtaining the `this` context of metaInfo props when
|
|
||||||
* declared in function form.
|
|
||||||
*
|
|
||||||
* @param {Object} component - the component object
|
|
||||||
* @return {Object} - the merged data
|
|
||||||
*/
|
|
||||||
export default function mergeComponentData (component) {
|
|
||||||
if (component.$children.length) {
|
|
||||||
return component.$children.reduce((data, child) => {
|
|
||||||
return assign({}, data, mergeComponentData(child))
|
|
||||||
}, component)
|
|
||||||
}
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import getMetaInfo from './getMetaInfo'
|
import $meta from './$meta'
|
||||||
import $meta from '../server/$meta'
|
|
||||||
import updateClientMetaInfo from '../client/updateClientMetaInfo'
|
|
||||||
|
|
||||||
// automatic install
|
// automatic install
|
||||||
if (typeof Vue !== 'undefined') {
|
if (typeof Vue !== 'undefined') {
|
||||||
@@ -26,10 +24,7 @@ export default function VueMeta (Vue) {
|
|||||||
|
|
||||||
requestId = window.requestAnimationFrame(() => {
|
requestId = window.requestAnimationFrame(() => {
|
||||||
requestId = null
|
requestId = null
|
||||||
const info = getMetaInfo(this.$root)
|
this.$meta().refresh()
|
||||||
|
|
||||||
// update the meta info
|
|
||||||
updateClientMetaInfo(info)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ describe('getComponentOption', () => {
|
|||||||
|
|
||||||
it('returns an empty object when no matching options are found', () => {
|
it('returns an empty object when no matching options are found', () => {
|
||||||
component = new Vue()
|
component = new Vue()
|
||||||
const fetchedOption = getComponentOption({ component, option: 'noop' })
|
const { mergedOption } = getComponentOption({ component, option: 'noop' })
|
||||||
expect(fetchedOption).to.eql({})
|
expect(mergedOption).to.eql({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fetches the given option from the given component', () => {
|
it('fetches the given option from the given component', () => {
|
||||||
component = new Vue({ someOption: 'foo' })
|
component = new Vue({ someOption: 'foo' })
|
||||||
const fetchedOption = getComponentOption({ component, option: 'someOption' })
|
const { mergedOption } = getComponentOption({ component, option: 'someOption' })
|
||||||
expect(fetchedOption).to.eql('foo')
|
expect(mergedOption).to.eql('foo')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fetches deeply nested component options and merges them', () => {
|
it('fetches deeply nested component options and merges them', () => {
|
||||||
@@ -28,8 +28,8 @@ describe('getComponentOption', () => {
|
|||||||
el: container
|
el: container
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchedOption = getComponentOption({ component, option: 'foo', deep: true })
|
const { mergedOption } = getComponentOption({ component, option: 'foo', deep: true })
|
||||||
expect(fetchedOption).to.eql({ bar: 'baz', fizz: 'buzz' })
|
expect(mergedOption).to.eql({ bar: 'baz', fizz: 'buzz' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows for a custom array merge strategy', () => {
|
it('allows for a custom array merge strategy', () => {
|
||||||
@@ -48,7 +48,7 @@ describe('getComponentOption', () => {
|
|||||||
el: container
|
el: container
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchedOption = getComponentOption({
|
const { mergedOption } = getComponentOption({
|
||||||
component,
|
component,
|
||||||
option: 'foo',
|
option: 'foo',
|
||||||
deep: true,
|
deep: true,
|
||||||
@@ -57,7 +57,7 @@ describe('getComponentOption', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(fetchedOption).to.eql([
|
expect(mergedOption).to.eql([
|
||||||
{ name: 'flower', content: 'tulip' },
|
{ name: 'flower', content: 'tulip' },
|
||||||
{ name: 'flower', content: 'rose' }
|
{ name: 'flower', content: 'rose' }
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user