Skip to content

Commit

Permalink
feat: Add props withDelay, on and force props
Browse files Browse the repository at this point in the history
- `withDelay` - Scheduled or delayed hydration
- `on` - Hydrate on specific event[s]
- `force` - Controlled hydration
  • Loading branch information
znck committed Jan 19, 2019
1 parent 61f5b74 commit 651c531
Show file tree
Hide file tree
Showing 7 changed files with 3,744 additions and 106 deletions.
5 changes: 3 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"presets": ["@babel/env"]
}
"presets": ["@babel/preset-env"],
"plugins": ["@znck/prop-types/remove"]
}
178 changes: 112 additions & 66 deletions Hydrate.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script>
import PropTypes from "@znck/prop-types";
const isBrowser = typeof window !== "undefined";
const io =
Expand All @@ -15,97 +17,128 @@ const io =
export default {
props: {
onClick: Boolean,
onHover: Boolean,
onInteraction: Boolean,
whenVisible: Boolean,
whenIdle: Boolean,
ssrOnly: Boolean
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
}),
beforeCreate() {
if (isBrowser) {
const render = this.$options.render;
this.$options.render = (...args) => {
const vnode = render.apply(this, args);
vnode.asyncFactory = this.hydrated ? { resolved: true } : {};
vnode.isAsyncPlaceholder = !this.hydrated;
created() {
if (isBrowser) console.log('Is browser')
else console.log('Is server')
PropTypes.validate(() => {
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'.`
);
}
return vnode;
};
}
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.childElementCount === 0) {
// No SSR rendered content.
this.hydrated = true;
// No SSR rendered content. Render now.
this.hydrate();
return;
}
if (this.ssrOnly) return;
if (this.whenIdle) {
const id = requestIdleCallback(
() => {
requestAnimationFrame(() => {
this.hydrate();
});
},
{ timeout: 500 }
);
let withDelay;
const on = this.on ? [this.on].flat() : [];
this.idle = () => cancelIdleCallback(id);
if (this.onClick) {
on.push("click");
}
if (this.whenVisible) {
this.$el.hydrate = this.hydrate;
if (this.onHover || this.onInteraction) {
on.push("mouseenter");
if (io) io.observe(this.$el.children[0]);
else console.warn("IntersectionObserver polyfill is required.");
if (this.onInteraction) {
on.push("focus");
}
}
this.visible = () => {
io && io.unobserve(this.$el);
delete this.$el.hydrate;
};
if (this.whenIdle) {
if (typeof requestIdleCallback !== "undefined") {
const id = requestIdleCallback(
() => {
requestAnimationFrame(() => {
this.hydrate();
});
},
{ timeout: 500 }
);
this.idle = () => cancelIdleCallback(id);
} else withDelay = 2000;
}
if (this.onClick) {
this.$el.addEventListener("click", this.hydrate, {
capture: true,
once: true
});
this.click = () => this.$el.removeEventListener("click", this.hydrate);
if (this.whenVisible) {
// As root node does not have any box model, it cannot intersect.
const el = this.$el.children[0];
el.hydrate = this.hydrate;
if (io) io.observe(el);
else {
withDelay = 2000;
PropTypes.validate(() =>
console.warn("IntersectionObserver polyfill is required.")
);
}
this.visible = () => {
io && io.unobserve(el);
delete el.hydrate;
};
}
if (this.onHover || this.onInteraction) {
this.$el.addEventListener("mouseenter", this.hydrate, {
capture: true,
once: true
});
this.hover = () =>
this.$el.removeEventListener("mouseenter", this.hydrate);
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.onInteraction) {
this.$el.addEventListener("focus", this.hydrate, {
capture: true,
once: true
});
this.interaction = () =>
this.$el.removeEventListener("focus", 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", "click", "hover", "interaction"];
const handlers = ["visible", "idle", "delay", "off"];
for (const handler of handlers) {
if (handler in this) {
Expand All @@ -118,13 +151,26 @@ export default {
this.hydrated = true;
this.cleanup();
}
},
render(h) {
const vnode = this.hydrated
? h("div", { staticStyle: "display: contents" }, this.$slots.default)
: h("div");
if (isBrowser) {
vnode.asyncFactory = this.hydrated ? { resolved: true } : {};
vnode.isAsyncPlaceholder = !this.hydrated;
}
return vnode;
},
watch: {
force: {
handler(value) {
if (value) this.hydrate();
},
immediate: true
}
}
};
</script>

<template>
<div v-if="hydrated" style="display: contents">
<slot/>
</div>
<div v-else style="display: contents"/>
</template>
20 changes: 20 additions & 0 deletions Hydrate.vue.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component } from 'vue'

const Hydrate: Component<
{ hydrated: boolean },
{ hydrate(): void },
{},
{
on?: string
onClick: boolean
onHover: boolean
onInteraction: boolean
whenVisible: boolean
whenIdle: boolean
withDelay: number
ssrOnly: boolean
force: boolean
}
> & { functional: false }

export default Hydrate
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

Lazy hydration for Vue SSR.


## Usage

### Installation
Expand All @@ -41,16 +40,28 @@ documents the different options provided:
```html
<template>
<div>
<!-- Hydrate when user clicks. -->
<Hydrate on-click>
<MyComponent />
<MyComponent />
<MyComponent />
</Hydrate>

<!-- Just in time hydration. When user hovers over content -->
<Hydrate on-hover>
...
</Hydrate>

<!-- Hydrate on any event -->
<Hydrate on="fullscreen">
...
</Hydrate>

<!-- or events -->
<Hydrate :on="['fullscreen', 'mousedown']">
...
</Hydrate>

<!-- When user hover over or keyboard focus into. -->
<Hydrate on-interaction>
...
Expand All @@ -70,10 +81,42 @@ documents the different options provided:
<Hydrate ssr-only>
...
</Hydrate>

<!-- Scheduled hydration. In 2s of initial render. -->
<Hydrate :with-delay="2000">
...
</Hydrate>

<!-- Controlled hydration -->
<Hydrate :force="isItReady">
...
</Hydrate>
</div>
</template>
```

Programmatic approach:

``` html
<template>
<Hydrate ref="child">
...
</Hydrate>
</template>

<script>
export default {
methods: {
foo() {
this.$refs.child.hydrate()
}
}
}
</script>
```

> **NOTE:** Consider BETA until v1.0 release.
## Prior Art

- [vue-lazy-hydration](https://github.com/maoberlehner/vue-lazy-hydration) by Markus Oberlehner
Expand Down
25 changes: 24 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,27 @@
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"@vue/server-test-utils": "^1.0.0-beta.28",
"@vue/test-utils": "^1.0.0-beta.28",
"@znck/prop-types": "^0.4.0",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"jest": "^23.6.0",
"regenerator-runtime": "^0.13.1",
"rollup": "^1.1.0",
"rollup-plugin-babel": "^4.3.1",
"rollup-plugin-vue": "^4.6.1",
"vue": "^2.5.22",
"vue-jest": "^3.0.2",
"vue-server-renderer": "^2.5.22",
"vue-template-compiler": "^2.5.22"
},
"scripts": {
"test": "jest",
"pre:build": "rm -rf dist/",
":build": "rollup -c --environment BUILD:production",
"prepublishOnly": "npm run :build",
"pre:release": "npm run test",
":release": "standard-version",
"post:release": "git push --follow-tags origin master && npm publish",
"release": "npm run :release"
Expand All @@ -45,5 +57,16 @@
"browserslist": "> 0.25%, not dead",
"main": "dist/lazy-hydration.ssr.js",
"module": "dist/lazy-hydration.js",
"browser": "Hydrate.vue"
"browser": "Hydrate.vue",
"jest": {
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"transform": {
".*\\.(vue)$": "vue-jest",
"^.+\\.js$": "babel-jest"
}
}
}
2 changes: 2 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default [
file: pkg.main,
},
],
external: ['@znck/prop-types']
},
{
input: 'Hydrate.vue',
Expand All @@ -36,5 +37,6 @@ export default [
file: pkg.module,
},
],
external: ['@znck/prop-types']
},
]
Loading

0 comments on commit 651c531

Please sign in to comment.