Skip to content

Commit

Permalink
Add Import Wrappers again
Browse files Browse the repository at this point in the history
  • Loading branch information
maoberlehner committed Mar 2, 2019
1 parent f954149 commit ef30466
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 17 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,59 @@ Internally the [Intersection Observer API](https://developer.mozilla.org/en-US/d

For a list of possible options please [take a look at the Intersection Observer API documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver).

## Import Wrappers

> **Attention:** because of [a bug in Vue.js <= v2.6.7](https://github.com/vuejs/vue/pull/9572) Import Wrappers require that you have at least version **v2.6.8** of Vue.js installed otherwise they will not work correctly in certain situations (especially in combination with Vue Router).
Additionally to the `<LazyHydrate>` wrapper component you can also use Import Wrappers to lazy load and hydrate certain components.

```html
<template>
<div class="ArticlePage">
<ImageSlider/>
<ArticleContent :content="article.content"/>
<AdSlider/>
<CommentForm :article-id="article.id"/>
</div>
</template>

<script>
import {
hydrateOnInteraction,
hydrateSsrOnly,
hydrateWhenIdle,
hydrateWhenVisible,
} from 'vue-lazy-hydration';
export default {
components: {
AdSlider: hydrateWhenVisible(
() => import('./AdSlider.vue'),
// Optional.
{ observerOptions: { rootMargin: '100px' } },
),
ArticleContent: hydrateSsrOnly(
() => import('./ArticleContent.vue'),
{ ignoredProps: ['content'] },
),
CommentForm: hydrateOnInteraction(
() => import('./CommentForm.vue'),
// `focus` is the default event.
{ event: 'focus', ignoredProps: ['articleId'] },
),
ImageSlider: hydrateWhenIdle(() => import('./ImageSlider.vue')),
},
// ...
};
</script>
```

### Caveats

1. Properties passed to a wrapped component are rendered as an HTML attribute on the root element.
E.g. `<ArticleContent :content="article.content"/>` would render to `<div class="ArticleContent" content="Lorem ipsum dolor ...">Lorem ipsum dolor ...</div>` as long as you don't provide `content` as an ignored property the way you can see in the example above.
2. When using `hydrateWhenVisible` and `hydrateOnInteraction` all instances of a certain component are immediately hydrated as soon as one of the instances becomes visible or is interacted with.

## Benchmarks

### Without lazy hydration
Expand Down
117 changes: 100 additions & 17 deletions src/LazyHydrate.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,111 @@
import {
createObserver,
loadingComponentFactory,
resolvableComponentFactory,
} from './utils';

const isServer = typeof window === `undefined`;
const isBrowser = !isServer;

const observers = new Map();
export function hydrateWhenIdle(component, { ignoredProps }) {
if (isServer) return component;

const resolvableComponent = resolvableComponentFactory(component);
const loading = loadingComponentFactory(resolvableComponent, {
props: ignoredProps,
mounted() {
// If `requestIdleCallback()` or `requestAnimationFrame()`
// is not supported, hydrate immediately.
if (!(`requestIdleCallback` in window) || !(`requestAnimationFrame` in window)) {
// eslint-disable-next-line no-underscore-dangle
resolvableComponent._resolve();
return;
}

const id = requestIdleCallback(() => {
// eslint-disable-next-line no-underscore-dangle
requestAnimationFrame(resolvableComponent._resolve);
}, { timeout: this.idleTimeout });
const cleanup = () => cancelIdleCallback(id);
resolvableComponent.then(cleanup);
},
});

function createObserver(options) {
if (typeof IntersectionObserver === `undefined`) return null;
return () => ({
component: resolvableComponent,
delay: 0,
loading,
});
}

const optionKey = JSON.stringify(options);
if (observers.has(optionKey)) return observers.get(optionKey);
export function hydrateWhenVisible(component, { ignoredProps, observerOptions }) {
if (isServer) return component;

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// Use `intersectionRatio` because of Edge 15's
// lack of support for `isIntersecting`.
// See: https://github.com/w3c/IntersectionObserver/issues/211
const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0;
if (!isIntersecting || !entry.target.parentElement.hydrate) return;
const resolvableComponent = resolvableComponentFactory(component);
const observer = createObserver(observerOptions);

entry.target.parentElement.hydrate();
});
}, options);
observers.set(optionKey, observer);
const loading = loadingComponentFactory(resolvableComponent, {
props: ignoredProps,
mounted() {
// If Intersection Observer API is not supported, hydrate immediately.
if (!observer) {
// eslint-disable-next-line no-underscore-dangle
resolvableComponent._resolve();
return;
}

// eslint-disable-next-line no-underscore-dangle
this.$el.hydrate = resolvableComponent._resolve;
const cleanup = () => observer.unobserve(this.$el);
resolvableComponent.then(cleanup);
observer.observe(this.$el);
},
});

return () => ({
component: resolvableComponent,
delay: 0,
loading,
});
}

export function hydrateSsrOnly(component) {
if (isServer) return component;

const resolvableComponent = resolvableComponentFactory(component);
const loading = loadingComponentFactory(resolvableComponent);

return () => ({
component: resolvableComponent,
delay: 0,
loading,
});
}

export function hydrateOnInteraction(component, { event = `focus`, ignoredProps }) {
if (isServer) return component;

const resolvableComponent = resolvableComponentFactory(component);
const events = Array.isArray(event) ? event : [event];

const loading = loadingComponentFactory(resolvableComponent, {
props: ignoredProps,
mounted() {
events.forEach((eventName) => {
// eslint-disable-next-line no-underscore-dangle
this.$el.addEventListener(eventName, resolvableComponent._resolve, {
capture: true,
once: true,
});
});
},
});

return observer;
return () => ({
component: resolvableComponent,
delay: 0,
loading,
});
}

export default {
Expand Down
49 changes: 49 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const observers = new Map();

export function createObserver(options) {
if (typeof IntersectionObserver === `undefined`) return null;

const optionKey = JSON.stringify(options);
if (observers.has(optionKey)) return observers.get(optionKey);

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// Use `intersectionRatio` because of Edge 15's
// lack of support for `isIntersecting`.
// See: https://github.com/w3c/IntersectionObserver/issues/211
const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0;
if (!isIntersecting || !entry.target.hydrate) return;
entry.target.hydrate();
});
}, options);
observers.set(optionKey, observer);

return observer;
}

export function loadingComponentFactory(resolvableComponent, options) {
return {
render(h) {
const tag = this.$el ? this.$el.tagName : `div`;

// eslint-disable-next-line no-underscore-dangle
if (!this.$el) resolvableComponent._resolve();

return h(tag);
},
...options,
};
}

export function resolvableComponentFactory(component) {
let resolve;
const promise = new Promise((newResolve) => {
resolve = newResolve;
});
// eslint-disable-next-line no-underscore-dangle
promise._resolve = async () => {
resolve(typeof component === `function` ? await component() : component);
};

return promise;
}

0 comments on commit ef30466

Please sign in to comment.