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:
+1
-1
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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}>` : '')
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user