2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-24 02:50:34 +03:00

support custom options

This commit is contained in:
Declan de Wet
2016-11-10 17:37:44 +02:00
parent 509583873e
commit d218afa2e6
14 changed files with 376 additions and 324 deletions
+5 -3
View File
@@ -1,6 +1,7 @@
import getMetaInfo from '../shared/getMetaInfo' import getMetaInfo from '../shared/getMetaInfo'
import updateClientMetaInfo from './updateClientMetaInfo' import updateClientMetaInfo from './updateClientMetaInfo'
export default function _refresh (options) {
/** /**
* When called, will update the current meta info with new meta info. * When called, will update the current meta info with new meta info.
* Useful when updating meta info as the result of an asynchronous * Useful when updating meta info as the result of an asynchronous
@@ -11,8 +12,9 @@ import updateClientMetaInfo from './updateClientMetaInfo'
* *
* @return {Object} - new meta info * @return {Object} - new meta info
*/ */
export default function refresh () { return function refresh () {
const info = getMetaInfo(this.$root) const info = getMetaInfo(options)(this.$root)
updateClientMetaInfo(info) updateClientMetaInfo(options)(info)
return info return info
} }
}
+10 -7
View File
@@ -1,17 +1,19 @@
import updateTitle from './updaters/updateTitle' import updateTitle from './updaters/updateTitle'
import updateTagAttributes from './updaters/updateTagAttributes' import updateTagAttributes from './updaters/updateTagAttributes'
import updateTags from './updaters/updateTags' import updateTags from './updaters/updateTags'
import { SERVER_RENDERED_ATTRIBUTE } from '../shared/constants'
export default function _updateClientMetaInfo (options) {
const { ssrAttribute } = options
/** /**
* Performs client-side updates when new meta info is received * Performs client-side updates when new meta info is received
* *
* @param {Object} newInfo - the meta info to update to * @param {Object} newInfo - the meta info to update to
*/ */
export default function updateClientMetaInfo (newInfo) { return function updateClientMetaInfo (newInfo) {
const htmlTag = document.getElementsByTagName('html')[0] const htmlTag = document.getElementsByTagName('html')[0]
// if this is not a server render, then update // if this is not a server render, then update
if (htmlTag.getAttribute(SERVER_RENDERED_ATTRIBUTE) === null) { if (htmlTag.getAttribute(ssrAttribute) === null) {
// initialize tracked changes // initialize tracked changes
const addedTags = {} const addedTags = {}
const removedTags = {} const removedTags = {}
@@ -20,12 +22,12 @@ export default function updateClientMetaInfo (newInfo) {
switch (key) { switch (key) {
// update the title // update the title
case 'title': case 'title':
updateTitle(newInfo.title) updateTitle(options)(newInfo.title)
break break
// update attributes // update attributes
case 'htmlAttrs': case 'htmlAttrs':
case 'bodyAttrs': case 'bodyAttrs':
updateTagAttributes(newInfo[key], key === 'htmlAttrs' ? htmlTag : document.getElementsByTagName('body')[0]) updateTagAttributes(options)(newInfo[key], key === 'htmlAttrs' ? htmlTag : document.getElementsByTagName('body')[0])
break break
// ignore these // ignore these
case 'titleChunk': case 'titleChunk':
@@ -34,7 +36,7 @@ export default function updateClientMetaInfo (newInfo) {
break break
// catch-all update tags // catch-all update tags
default: default:
const { oldTags, newTags } = updateTags(key, newInfo[key], document.getElementsByTagName('head')[0]) const { oldTags, newTags } = updateTags(options)(key, newInfo[key], document.getElementsByTagName('head')[0])
if (newTags.length) { if (newTags.length) {
addedTags[key] = newTags addedTags[key] = newTags
removedTags[key] = oldTags removedTags[key] = oldTags
@@ -48,6 +50,7 @@ export default function updateClientMetaInfo (newInfo) {
} }
} else { } else {
// remove the server render attribute so we can update on changes // remove the server render attribute so we can update on changes
htmlTag.removeAttribute(SERVER_RENDERED_ATTRIBUTE) htmlTag.removeAttribute(ssrAttribute)
}
} }
} }
+6 -6
View File
@@ -1,13 +1,12 @@
import { VUE_META_ATTRIBUTE } from '../../shared/constants' export default function _updateTagAttributes ({ attribute }) {
/** /**
* updates the document's html tag attributes * updates the document's html tag attributes
* *
* @param {Object} attrs - the new document html attributes * @param {Object} attrs - the new document html attributes
* @param {HTMLElement} tag - the HTMLElment tag to update with new attrs * @param {HTMLElement} tag - the HTMLElment tag to update with new attrs
*/ */
export default function updateTagAttributes (attrs, tag) { return function updateTagAttributes (attrs, tag) {
const vueMetaAttrString = tag.getAttribute(VUE_META_ATTRIBUTE) const vueMetaAttrString = tag.getAttribute(attribute)
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : [] const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
const toRemove = [].concat(vueMetaAttrs) const toRemove = [].concat(vueMetaAttrs)
for (let attr in attrs) { for (let attr in attrs) {
@@ -28,8 +27,9 @@ export default function updateTagAttributes (attrs, tag) {
tag.removeAttribute(toRemove[i]) tag.removeAttribute(toRemove[i])
} }
if (vueMetaAttrs.length === toRemove.length) { if (vueMetaAttrs.length === toRemove.length) {
tag.removeAttribute(VUE_META_ATTRIBUTE) tag.removeAttribute(attribute)
} else { } else {
tag.setAttribute(VUE_META_ATTRIBUTE, vueMetaAttrs.join(',')) tag.setAttribute(attribute, vueMetaAttrs.join(','))
}
} }
} }
+9 -9
View File
@@ -1,8 +1,7 @@
import { VUE_META_ATTRIBUTE } from '../../shared/constants'
// borrow the slice method // borrow the slice method
const toArray = Function.prototype.call.bind(Array.prototype.slice) const toArray = Function.prototype.call.bind(Array.prototype.slice)
export default function _updateTags ({ attribute }) {
/** /**
* Updates meta tags inside <head> on the client. Borrowed from `react-helmet`: * Updates meta tags inside <head> on the client. Borrowed from `react-helmet`:
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245 * https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
@@ -11,8 +10,8 @@ const toArray = Function.prototype.call.bind(Array.prototype.slice)
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base * @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - a representation of what tags changed * @return {Object} - a representation of what tags changed
*/ */
export default function updateTags (type, tags, headTag) { return function updateTags (type, tags, headTag) {
const nodes = headTag.querySelectorAll(`${type}[${VUE_META_ATTRIBUTE}]`) const nodes = headTag.querySelectorAll(`${type}[${attribute}]`)
const oldTags = toArray(nodes) const oldTags = toArray(nodes)
const newTags = [] const newTags = []
let indexToDelete let indexToDelete
@@ -21,8 +20,8 @@ export default function updateTags (type, tags, headTag) {
tags.forEach((tag) => { tags.forEach((tag) => {
const newElement = document.createElement(type) const newElement = document.createElement(type)
for (const attribute in tag) { for (const attr in tag) {
if (tag.hasOwnProperty(attribute)) { if (tag.hasOwnProperty(attr)) {
if (attribute === 'innerHTML') { if (attribute === 'innerHTML') {
newElement.innerHTML = tag.innerHTML newElement.innerHTML = tag.innerHTML
} else if (attribute === 'cssText') { } else if (attribute === 'cssText') {
@@ -32,13 +31,13 @@ export default function updateTags (type, tags, headTag) {
newElement.appendChild(document.createTextNode(tag.cssText)) newElement.appendChild(document.createTextNode(tag.cssText))
} }
} else { } else {
const value = (typeof tag[attribute] === 'undefined') ? '' : tag[attribute] const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
newElement.setAttribute(attribute, value) newElement.setAttribute(attr, value)
} }
} }
} }
newElement.setAttribute(VUE_META_ATTRIBUTE, 'true') newElement.setAttribute(attribute, 'true')
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared. // Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
if (oldTags.some((existingTag, index) => { if (oldTags.some((existingTag, index) => {
@@ -57,3 +56,4 @@ export default function updateTags (type, tags, headTag) {
return { oldTags, newTags } return { oldTags, newTags }
} }
}
+3 -1
View File
@@ -1,8 +1,10 @@
export default function _updateTitle () {
/** /**
* updates the document title * updates the document title
* *
* @param {String} title - the new title of the document * @param {String} title - the new title of the document
*/ */
export default function updateTitle (title = document.title) { return function updateTitle (title = document.title) {
document.title = title document.title = title
} }
}
+6 -4
View File
@@ -2,6 +2,7 @@ import titleGenerator from './generators/titleGenerator'
import attrsGenerator from './generators/attrsGenerator' import attrsGenerator from './generators/attrsGenerator'
import tagGenerator from './generators/tagGenerator' import tagGenerator from './generators/tagGenerator'
export default function _generateServerInjector (options) {
/** /**
* Converts a meta info property to one that can be stringified on the server * Converts a meta info property to one that can be stringified on the server
* *
@@ -9,14 +10,15 @@ import tagGenerator from './generators/tagGenerator'
* @param {(String|Object|Array<Object>)} data - the data value * @param {(String|Object|Array<Object>)} data - the data value
* @return {Object} - the new injector * @return {Object} - the new injector
*/ */
export default function generateServerInjector (type, data) { return function generateServerInjector (type, data) {
switch (type) { switch (type) {
case 'title': case 'title':
return titleGenerator(type, data) return titleGenerator(options)(type, data)
case 'htmlAttrs': case 'htmlAttrs':
case 'bodyAttrs': case 'bodyAttrs':
return attrsGenerator(type, data) return attrsGenerator(options)(type, data)
default: default:
return tagGenerator(type, data) return tagGenerator(options)(type, data)
}
} }
} }
+4 -4
View File
@@ -1,5 +1,4 @@
import { VUE_META_ATTRIBUTE } from '../../shared/constants' export default function _attrsGenerator ({ attribute }) {
/** /**
* Generates tag attributes for use on the server. * Generates tag attributes for use on the server.
* *
@@ -7,7 +6,7 @@ import { VUE_META_ATTRIBUTE } from '../../shared/constants'
* @param {Object} data - the attributes to generate * @param {Object} data - the attributes to generate
* @return {Object} - the attribute generator * @return {Object} - the attribute generator
*/ */
export default function attrsGenerator (type, data) { return function attrsGenerator (type, data) {
return { return {
text () { text () {
let attributeStr = '' let attributeStr = ''
@@ -22,8 +21,9 @@ export default function attrsGenerator (type, data) {
} ` } `
} }
} }
attributeStr += `${VUE_META_ATTRIBUTE}="${watchedAttrs.join(',')}"` attributeStr += `${attribute}="${watchedAttrs.join(',')}"`
return attributeStr.trim() return attributeStr.trim()
} }
} }
} }
}
+5 -5
View File
@@ -1,5 +1,4 @@
import { VUE_META_ATTRIBUTE } from '../../shared/constants' export default function _tagGenerator ({ attribute }) {
/** /**
* Generates meta, base, link, style, script, noscript tags for use on the server * Generates meta, base, link, style, script, noscript tags for use on the server
* *
@@ -7,7 +6,7 @@ import { VUE_META_ATTRIBUTE } from '../../shared/constants'
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base * @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - the tag generator * @return {Object} - the tag generator
*/ */
export default function tagGenerator (type, tags) { return function tagGenerator (type, tags) {
return { return {
text () { text () {
// build a string containing all tags of this type // build a string containing all tags of this type
@@ -36,9 +35,10 @@ export default function tagGenerator (type, tags) {
// the final string for this specific tag // the final string for this specific tag
return closed return closed
? `${tagsStr}<${type} ${VUE_META_ATTRIBUTE}="true" ${attrs}/>` ? `${tagsStr}<${type} ${attribute}="true" ${attrs}/>`
: `${tagsStr}<${type} ${VUE_META_ATTRIBUTE}="true" ${attrs}>${content}</${type}>` : `${tagsStr}<${type} ${attribute}="true" ${attrs}>${content}</${type}>`
}, '') }, '')
} }
} }
} }
}
+4 -4
View File
@@ -1,5 +1,4 @@
import { VUE_META_ATTRIBUTE } from '../../shared/constants' export default function _titleGenerator ({ attribute }) {
/** /**
* Generates title output for the server * Generates title output for the server
* *
@@ -7,10 +6,11 @@ import { VUE_META_ATTRIBUTE } from '../../shared/constants'
* @param {String} data - the title text * @param {String} data - the title text
* @return {Object} - the title generator * @return {Object} - the title generator
*/ */
export default function titleGenerator (type, data) { return function titleGenerator (type, data) {
return { return {
text () { text () {
return `<${type} ${VUE_META_ATTRIBUTE}="true">${data}</${type}>` return `<${type} ${attribute}="true">${data}</${type}>`
}
} }
} }
} }
+5 -3
View File
@@ -1,6 +1,7 @@
import getMetaInfo from '../shared/getMetaInfo' import getMetaInfo from '../shared/getMetaInfo'
import generateServerInjector from './generateServerInjector' import generateServerInjector from './generateServerInjector'
export default function _inject (options) {
/** /**
* Converts the state of the meta info object such that each item * Converts the state of the meta info object such that each item
* can be compiled to a tag string on the server * can be compiled to a tag string on the server
@@ -8,16 +9,17 @@ import generateServerInjector from './generateServerInjector'
* @this {Object} - Vue instance - ideally the root component * @this {Object} - Vue instance - ideally the root component
* @return {Object} - server meta info with `toString` methods * @return {Object} - server meta info with `toString` methods
*/ */
export default function inject () { return function inject () {
// get meta info with sensible defaults // get meta info with sensible defaults
const info = getMetaInfo(this.$root) const info = getMetaInfo(options)(this.$root)
// generate server injectors // generate server injectors
for (let key in info) { for (let key in info) {
if (info.hasOwnProperty(key) && key !== 'titleTemplate') { if (info.hasOwnProperty(key) && key !== 'titleTemplate') {
info[key] = generateServerInjector(key, info[key]) info[key] = generateServerInjector(options)(key, info[key])
} }
} }
return info return info
} }
}
+5 -4
View File
@@ -1,15 +1,16 @@
import inject from '../server/inject' import inject from '../server/inject'
import refresh from '../client/refresh' import refresh from '../client/refresh'
export default function _$meta (options) {
/** /**
* Returns an injector for server-side rendering. * Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component) * @this {Object} - the Vue instance (a root component)
* @return {Object} - injector * @return {Object} - injector
*/ */
export default function $meta () { return function $meta () {
// bind inject method to this component
return { return {
inject: inject.bind(this), inject: inject(options).bind(this),
refresh: refresh.bind(this) refresh: refresh(options).bind(this)
}
} }
} }
+20 -1
View File
@@ -1,2 +1,21 @@
/**
* These are constant variables used throughout the application.
*/
// This is the name of the component option that contains all the information that
// gets converted to the various meta tags & attributes for the page.
export const VUE_META_KEY_NAME = 'metaInfo'
// This is the attribute vue-meta augments on elements to know which it should
// manage and which it should ignore.
export const VUE_META_ATTRIBUTE = 'data-vue-meta' export const VUE_META_ATTRIBUTE = 'data-vue-meta'
export const SERVER_RENDERED_ATTRIBUTE = 'data-vue-meta-server-rendered'
// This is the attribute that goes on the `html` tag to inform `vue-meta`
// that the server has already generated the meta tags for the initial render.
export const VUE_META_SERVER_RENDERED_ATTRIBUTE = 'data-vue-meta-server-rendered'
// This is the property that tells vue-meta to overwrite (instead of append)
// an item in a tag list. For example, if you have two `meta` tag list items
// that both have `vmid` of "description", then vue-meta will overwrite the
// shallowest one with the deepest one.
export const VUE_META_TAG_LIST_ID_KEY_NAME = 'vmid'
+5 -3
View File
@@ -1,6 +1,7 @@
import deepmerge from 'deepmerge' import deepmerge from 'deepmerge'
import getComponentOption from './getComponentOption' import getComponentOption from './getComponentOption'
export default function _getMetaInfo ({ keyName, tagIDKeyName }) {
/** /**
* Returns the correct meta info for the given component * Returns the correct meta info for the given component
* (child components will overwrite parent meta info) * (child components will overwrite parent meta info)
@@ -8,7 +9,7 @@ import getComponentOption from './getComponentOption'
* @param {Object} component - the Vue instance to get meta info from * @param {Object} component - the Vue instance to get meta info from
* @return {Object} - returned meta info * @return {Object} - returned meta info
*/ */
export default function getMetaInfo (component) { return function getMetaInfo (component) {
// set some sane defaults // set some sane defaults
const defaultInfo = { const defaultInfo = {
title: '', title: '',
@@ -27,7 +28,7 @@ export default function getMetaInfo (component) {
// collect & aggregate all metaInfo $options // collect & aggregate all metaInfo $options
const info = getComponentOption({ const info = getComponentOption({
component, component,
option: 'metaInfo', option: keyName,
deep: true, deep: true,
arrayMerge (target, source) { arrayMerge (target, source) {
// we concat the arrays without merging objects contained therein, // we concat the arrays without merging objects contained therein,
@@ -41,7 +42,7 @@ export default function getMetaInfo (component) {
let shared = false let shared = false
for (let sourceIndex in source) { for (let sourceIndex in source) {
const sourceItem = source[sourceIndex] const sourceItem = source[sourceIndex]
if (targetItem.vmid === sourceItem.vmid) { if (targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
shared = true shared = true
break break
} }
@@ -73,3 +74,4 @@ export default function getMetaInfo (component) {
return deepmerge(defaultInfo, info) return deepmerge(defaultInfo, info)
} }
}
+21 -2
View File
@@ -1,17 +1,36 @@
import assign from 'object-assign'
import $meta from './$meta' import $meta from './$meta'
import {
VUE_META_KEY_NAME,
VUE_META_ATTRIBUTE,
VUE_META_SERVER_RENDERED_ATTRIBUTE,
VUE_META_TAG_LIST_ID_KEY_NAME
} from './constants'
// automatic install // automatic install
if (typeof Vue !== 'undefined') { if (typeof Vue !== 'undefined') {
Vue.use(VueMeta) Vue.use(VueMeta)
} }
// set some default options
const defaultOptions = {
keyName: VUE_META_KEY_NAME,
attribute: VUE_META_ATTRIBUTE,
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
}
/** /**
* Plugin install function. * Plugin install function.
* @param {Function} Vue - the Vue constructor. * @param {Function} Vue - the Vue constructor.
*/ */
export default function VueMeta (Vue) { export default function VueMeta (Vue, options = {}) {
// combine options
options = assign(defaultOptions, options)
// bind the $meta method to this component instance // bind the $meta method to this component instance
Vue.prototype.$meta = $meta Vue.prototype.$meta = $meta(options)
// store an id to keep track of DOM updates // store an id to keep track of DOM updates
let requestId = null let requestId = null