2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-18 18:12:07 +03:00

feat: enable onload callbacks (#414)

* refactor(examples): run ssr example from server

* chore: switch to babel for build

buble complains too much

* feat: enable loaded callbacks

feat: add skip option

* examples: add async-callback browser example

* examples: fix server

* examples(ssr): add reactive script with callback

* fix: also skip on ssr

* chore: remove unused var

* feat: only add mutationobserver if DOM is still loading

feat: disconnect mutation observer once DOM has loaded

* examples: pass vmid to loadCallback instead of el

* feat: also support load callbacks for link/style tags

* test: add unit tests for load

* test: add load e2e test

* chore: fix lint

* chore: remove unused files

* test: fix e2e load callback test

* test: fix attempt

* examples: ie9 compatiblity

destructuring doesnt work in ie9

* fix: add onload attribute on ssr

dont rely on mutationobserver

* chore: lint ci conf

* refactor: remove loadCallbackAttribute config option

test: fix coverage for load

* test: improve coverage

* fix: only use console when it exists (for ie9)

* chore: fix coverage
This commit is contained in:
Pim
2019-07-24 10:18:40 +02:00
committed by GitHub
parent 05163a77a8
commit fc71e1f1c4
49 changed files with 963 additions and 632 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { showWarningNotSupported } from '../shared/constants'
import { showWarningNotSupported } from '../shared/log'
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from './refresh'
+118
View File
@@ -0,0 +1,118 @@
import { toArray } from '../utils/array'
const callbacks = []
export function isDOMLoaded (d = document) {
return d.readyState !== 'loading'
}
export function isDOMComplete (d = document) {
return d.readyState === 'complete'
}
export function waitDOMLoaded () {
if (isDOMLoaded()) {
return true
}
return new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve))
}
export function addCallback (query, callback) {
if (arguments.length === 1) {
callback = query
query = ''
}
callbacks.push([ query, callback ])
}
export function addCallbacks ({ tagIDKeyName }, type, tags, autoAddListeners) {
let hasAsyncCallback = false
for (const tag of tags) {
if (!tag[tagIDKeyName] || !tag.callback) {
continue
}
hasAsyncCallback = true
addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback)
}
if (!autoAddListeners || !hasAsyncCallback) {
return hasAsyncCallback
}
return addListeners()
}
export function addListeners () {
if (isDOMComplete()) {
applyCallbacks()
return
}
// Instead of using a MutationObserver, we just apply
/* istanbul ignore next */
document.onreadystatechange = () => {
applyCallbacks()
}
}
export function applyCallbacks (matchElement) {
for (const [query, callback] of callbacks) {
const selector = `${query}[onload="this.__vm_l=1"]`
let elements = []
if (!matchElement) {
elements = toArray(document.querySelectorAll(selector))
}
if (matchElement && matchElement.matches(selector)) {
elements = [matchElement]
}
for (const element of elements) {
/* __vm_cb: whether the load callback has been called
* __vm_l: set by onload attribute, whether the element was loaded
* __vm_ev: whether the event listener was added or not
*/
if (element.__vm_cb) {
continue
}
const onload = () => {
/* Mark that the callback for this element has already been called,
* this prevents the callback to run twice in some (rare) conditions
*/
element.__vm_cb = true
/* onload needs to be removed because we only need the
* attribute after ssr and if we dont remove it the node
* will fail isEqualNode on the client
*/
element.removeAttribute('onload')
callback(element)
}
/* IE9 doesnt seem to load scripts synchronously,
* causing a script sometimes/often already to be loaded
* when we add the event listener below (thus adding an onload event
* listener has no use because it will never be triggered).
* Therefore we add the onload attribute during ssr, and
* check here if it was already loaded or not
*/
if (element.__vm_l) {
onload()
continue
}
if (!element.__vm_ev) {
element.__vm_ev = true
element.addEventListener('load', onload)
}
}
}
}
+15 -1
View File
@@ -1,7 +1,8 @@
import { metaInfoOptionKeys, metaInfoAttributeKeys } from '../shared/constants'
import { metaInfoOptionKeys, metaInfoAttributeKeys, tagsSupportingOnload } from '../shared/constants'
import { isArray } from '../utils/is-type'
import { includes } from '../utils/array'
import { getTag } from '../utils/elements'
import { addCallbacks, addListeners } from './load'
import { updateAttribute, updateTag, updateTitle } from './updaters'
/**
@@ -21,6 +22,19 @@ export default function updateClientMetaInfo (appId, options = {}, newInfo) {
if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) {
// remove the server render attribute so we can update on (next) changes
htmlTag.removeAttribute(ssrAttribute)
// add load callbacks if the
let addLoadListeners = false
for (const type of tagsSupportingOnload) {
if (newInfo[type] && addCallbacks(options, type, newInfo[type])) {
addLoadListeners = true
}
}
if (addLoadListeners) {
addListeners()
}
return false
}
+42 -28
View File
@@ -10,7 +10,9 @@ import { queryElements, getElementsKey } from '../../utils/elements.js'
* @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
*/
export default function updateTag (appId, { attribute, tagIDKeyName } = {}, type, tags, head, body) {
export default function updateTag (appId, options = {}, type, tags, head, body) {
const { attribute, tagIDKeyName } = options
const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
const newElements = []
@@ -36,38 +38,50 @@ export default function updateTag (appId, { attribute, tagIDKeyName } = {}, type
if (tags.length) {
for (const tag of tags) {
if (tag.skip) {
continue
}
const newElement = document.createElement(type)
newElement.setAttribute(attribute, appId)
for (const attr in tag) {
if (tag.hasOwnProperty(attr)) {
if (attr === 'innerHTML') {
newElement.innerHTML = tag.innerHTML
continue
}
if (attr === 'cssText') {
if (newElement.styleSheet) {
/* istanbul ignore next */
newElement.styleSheet.cssText = tag.cssText
} else {
newElement.appendChild(document.createTextNode(tag.cssText))
}
continue
}
const _attr = includes(dataAttributes, attr)
? `data-${attr}`
: attr
const isBooleanAttribute = includes(booleanHtmlAttributes, attr)
if (isBooleanAttribute && !tag[attr]) {
continue
}
const value = isBooleanAttribute ? '' : tag[attr]
newElement.setAttribute(_attr, value)
/* istanbul ignore next */
if (!tag.hasOwnProperty(attr)) {
continue
}
if (attr === 'innerHTML') {
newElement.innerHTML = tag.innerHTML
continue
}
if (attr === 'cssText') {
if (newElement.styleSheet) {
/* istanbul ignore next */
newElement.styleSheet.cssText = tag.cssText
} else {
newElement.appendChild(document.createTextNode(tag.cssText))
}
continue
}
if (attr === 'callback') {
newElement.onload = () => tag[attr](newElement)
continue
}
const _attr = includes(dataAttributes, attr)
? `data-${attr}`
: attr
const isBooleanAttribute = includes(booleanHtmlAttributes, attr)
if (isBooleanAttribute && !tag[attr]) {
continue
}
const value = isBooleanAttribute ? '' : tag[attr]
newElement.setAttribute(_attr, value)
}
const oldElements = currentElements[getElementsKey(tag)]
+28 -17
View File
@@ -1,4 +1,10 @@
import { booleanHtmlAttributes, tagsWithoutEndTag, tagsWithInnerContent, tagAttributeAsInnerContent, commonDataAttributes } from '../../shared/constants'
import {
booleanHtmlAttributes,
tagsWithoutEndTag,
tagsWithInnerContent,
tagAttributeAsInnerContent,
commonDataAttributes
} from '../../shared/constants'
/**
* Generates meta, base, link, style, script, noscript tags for use on the server
@@ -8,12 +14,16 @@ import { booleanHtmlAttributes, tagsWithoutEndTag, tagsWithInnerContent, tagAttr
* @return {Object} - the tag generator
*/
export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}, type, tags) {
const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
const dataAttributes = [tagIDKeyName, 'callback', ...commonDataAttributes]
return {
text ({ body = false, pbody = false } = {}) {
// build a string containing all tags of this type
return tags.reduce((tagsStr, tag) => {
if (tag.skip) {
return tagsStr
}
const tagKeys = Object.keys(tag)
if (tagKeys.length === 0) {
@@ -24,11 +34,13 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
return tagsStr
}
let attrs = tag.once ? '' : ` ${attribute}="${ssrAppId}"`
// build a string containing all attributes of this tag
const attrs = tagKeys.reduce((attrsStr, attr) => {
for (const attr in tag) {
// these attributes are treated as children on the tag
if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') {
return attrsStr
continue
}
// these form the attribute list for this tag
@@ -37,23 +49,23 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
prefix = 'data-'
}
const isBooleanAttr = booleanHtmlAttributes.includes(attr)
if (isBooleanAttr && !tag[attr]) {
return attrsStr
if (attr === 'callback') {
attrs += ` onload="this.__vm_l=1"`
continue
}
return isBooleanAttr
? `${attrsStr} ${prefix}${attr}`
: `${attrsStr} ${prefix}${attr}="${tag[attr]}"`
}, '')
const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr)
if (isBooleanAttr && !tag[attr]) {
continue
}
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
}
// grab child content from one of these attributes, if possible
const content = tag.innerHTML || tag.cssText || ''
// generate tag exactly without any other redundant attribute
const observeTag = tag.once
? ''
: `${attribute}="${ssrAppId}"`
// these tags have no end tag
const hasEndTag = !tagsWithoutEndTag.includes(type)
@@ -62,9 +74,8 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
// the final string for this specific tag
return !hasContent
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` +
(hasContent ? `${content}</${type}>` : '')
}, '')
}
}
+3 -3
View File
@@ -79,6 +79,9 @@ export const metaInfoAttributeKeys = [
'bodyAttrs'
]
// HTML elements which support the onload event
export const tagsSupportingOnload = ['link', 'style', 'script']
// HTML elements which dont have a head tag (shortened to our needs)
// see: https://www.w3.org/TR/html52/document-metadata.html
export const tagsWithoutEndTag = ['base', 'meta', 'link']
@@ -137,6 +140,3 @@ export const booleanHtmlAttributes = [
'typemustmatch',
'visible'
]
// eslint-disable-next-line no-console
export const showWarningNotSupported = () => console.warn('This vue app/component has no vue-meta configuration')
+16
View File
@@ -0,0 +1,16 @@
import { hasGlobalWindow } from '../utils/window'
const _global = hasGlobalWindow ? window : global
const console = (_global.console = _global.console || {})
export function warn (...args) {
/* istanbul ignore next */
if (!console || !console.warn) {
return
}
console.warn(...args)
}
export const showWarningNotSupported = () => warn('This vue app/component has no vue-meta configuration')
+4 -4
View File
@@ -1,7 +1,8 @@
import deepmerge from 'deepmerge'
import { findIndex } from '../utils/array'
import { includes, findIndex } from '../utils/array'
import { applyTemplate } from './template'
import { metaInfoAttributeKeys, booleanHtmlAttributes } from './constants'
import { warn } from './log'
export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, contentKeyName }, target, source) {
// we concat the arrays without merging objects contained in,
@@ -80,9 +81,8 @@ export function merge (target, source, options = {}) {
for (const key in source[attrKey]) {
if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) {
if (booleanHtmlAttributes.includes(key)) {
// eslint-disable-next-line no-console
console.warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details')
if (includes(booleanHtmlAttributes, key)) {
warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details')
}
delete source[attrKey][key]
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { isUndefined, isFunction } from '../utils/is-type'
import { ensuredPush } from '../utils/ensure'
import { hasMetaInfo } from './meta-helpers'
import { addNavGuards } from './nav-guards'
import { warn } from './log'
let appId = 1
@@ -18,7 +19,7 @@ export default function createMixin (Vue, options) {
get () {
// Show deprecation warning once when devtools enabled
if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) {
console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead') // eslint-disable-line no-console
warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead')
this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true
}
return hasMetaInfo(this)