UNPKG

lazy-hydration

Version:
191 lines (167 loc) 4.46 kB
/* eslint-disable no-console */ import PropTypes from '@znck/prop-types' const isBrowser = typeof window !== 'undefined' const io = typeof IntersectionObserver !== 'undefined' ? new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting || entry.intersectionRatio > 0) { entry.target.dispatchEvent(new CustomEvent('hydration:visible')) } }) }) : null const asyncFactoryResolved = { resolved: true } const asyncFactory = {} const clientSideDivData = { attrs: { 'data-force-hydrate': true, }, } export default { props: { on: PropTypes.oneOfType(String, PropTypes.arrayOf(String)), onClick: PropTypes.bool, onHover: PropTypes.bool, onInteraction: PropTypes.bool, whenVisible: PropTypes.bool, whenIdle: PropTypes.bool, withDelay: PropTypes.number, ssrOnly: PropTypes.bool, force: PropTypes.bool, }, data: () => ({ hydrated: !isBrowser, }), created() { PropTypes.run(() => { if ( !this.on && !this.onClick && !this.onHover && !this.onInteraction && !this.whenVisible && !this.whenIdle && !this.ssrOnly && this.force === undefined ) { console.error( `Select at least one trigger to enable hydration. If you don't want to hydrate at all use 'ssr-only'.` ) } if (this.withDelay && this.withDelay < 800) { console.warn( `Delay duration ${ this.withDelay }ms is too low. A good choice would be around 2000ms. See https://github.com/znck/lazy-hydration.` ) } }) }, mounted() { if (this.$el.dataset.forceHydrate) { // No SSR rendered content. Render now. this.hydrate() return } if (this.ssrOnly) return let withDelay const on = (Array.isArray(this.on) ? this.on : [this.on]) .slice() .filter(name => typeof name !== 'string') if (this.onClick) { on.push('click') } if (this.onHover || this.onInteraction) { on.push('mouseenter') if (this.onInteraction) { on.push('focus') } } if (this.whenIdle) { if (typeof requestIdleCallback !== 'undefined') { const id = requestIdleCallback( () => { requestAnimationFrame(() => { this.hydrate() }) }, { timeout: 500 } ) this.idle = () => cancelIdleCallback(id) } else withDelay = 2000 } if (this.whenVisible) { const el = this.$el // As root node does not have any box model, it cannot intersect. on.push('hydration:visible') if (io) io.observe(el) else { withDelay = 2000 PropTypes.run(() => console.warn('IntersectionObserver polyfill is required.') ) } this.visible = () => { io && io.unobserve(el) } } if (on.length) { on.forEach(event => this.$el.addEventListener(event, this.hydrate, { capture: true, once: true, }) ) this.off = () => on.forEach(event => this.$el.removeEventListener(event, this.hydrate)) } if (this.withDelay || withDelay) { const id = setTimeout(this.hydrate, this.withDelay || withDelay) this.delay = () => clearTimeout(id) } }, beforeDestroy() { this.cleanup() }, methods: { cleanup() { const handlers = ['visible', 'idle', 'delay', 'off'] for (const handler of handlers) { if (handler in this) { this[handler]() delete this[handler] } } }, hydrate() { this.hydrated = true this.cleanup() }, }, render(h) { const firstChild = () => { const children = this.$scopedSlots.default ? this.$scopedSlots.default({ hydrated: this.hydrated }) : this.$slots.default return Array.isArray(children) ? children[0] : children } const vnode = this.hydrated ? firstChild() : h('div', clientSideDivData) vnode.asyncFactory = this.hydrated ? asyncFactoryResolved : asyncFactory vnode.isComment = !this.hydrated if (isBrowser) { window.vnode = vnode } return vnode }, watch: { force: { handler(value) { if (value) this.hydrate() }, immediate: true, }, }, }