Skip to content

Commit

Permalink
Merge branch 'release/2.1.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
LinusBorg committed Apr 24, 2019
2 parents 3a23df9 + 9cc2dca commit 6521f47
Show file tree
Hide file tree
Showing 14 changed files with 89 additions and 38 deletions.
10 changes: 8 additions & 2 deletions dist/portal-vue.common.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/portal-vue.common.js.map

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions dist/portal-vue.esm.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/portal-vue.esm.js.map

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions dist/portal-vue.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/*!
* portal-vue © Thorsten Lünborg, 2019
*
* Version: 2.1.1
* Version: 2.1.2
*
* LICENCE: MIT
*
Expand Down Expand Up @@ -52,6 +52,7 @@
throw new TypeError("Invalid attempt to spread non-iterable instance");
}

var inBrowser = typeof window !== 'undefined';
function freeze(item) {
if (Array.isArray(item) || _typeof(item) === 'object') {
return Object.freeze(item);
Expand Down Expand Up @@ -95,11 +96,12 @@
transports: transports,
targets: targets,
sources: sources,
trackInstances: true
trackInstances: inBrowser
};
},
methods: {
open: function open(transport) {
if (!inBrowser) return;
var to = transport.to,
from = transport.from,
passengers = transport.passengers,
Expand Down Expand Up @@ -156,6 +158,8 @@
}
},
registerTarget: function registerTarget(target, vm, force) {
if (!inBrowser) return;

if (this.trackInstances && !force && this.targets[target]) {
console.warn("[portal-vue]: Target ".concat(target, " already exists"));
}
Expand All @@ -166,6 +170,8 @@
this.$delete(this.targets, target);
},
registerSource: function registerSource(source, vm, force) {
if (!inBrowser) return;

if (this.trackInstances && !force && this.sources[source]) {
console.warn("[portal-vue]: source ".concat(source, " already exists"));
}
Expand Down
4 changes: 2 additions & 2 deletions dist/portal-vue.umd.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: "What's new in 2.0", link: '/guide/migration' },
{ text: 'Advanced Usage', link: '/guide/advanced' },
{ text: 'SSR', link: '/guide/SSR' },
{ text: 'Caveats', link: '/guide/caveats' },
],
},
Expand Down
24 changes: 20 additions & 4 deletions docs/api/wormhole.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ The Wormhole is not a component, it's an object that connects the `<Portal>`s to

Usually, you will never need to use this object, but you _can_ use this object to programmatically send content to a `<PortalTarget>`, check weither a target exists, and other stuff.

:::tip Hint
This feature was introduced with version `1.1.0`
:::

:::warning Public API
The wormhole object exposes quite a few properties and methods. With regard to semver, only the properties and methods documented below are considered part of the public API.

Expand Down Expand Up @@ -62,6 +58,11 @@ This is the programmatic equivalent of the following:
</portal>
```

::: tip Server-Side Rendering
For the reasons layed out in [the section about SSR](../guide/SSR.md), this method won't do anything during Server-Side Rendering. Portal'ing of the content will only happen on the client.
Make sure to Read the linked section for more information.
:::

### close()

As the name suggests, this is the counterpart to `open()`. It's used to remove content from a `<PortalTarget>`.
Expand Down Expand Up @@ -122,6 +123,11 @@ Wormhole.hasSource('origin')
// => true/false
```

::: tip Server-Side Rendering
For the reasons layed out in [the section about SSR](../guide/SSR.md), this method will aleways return `false` during Server-Side Rendering.
Make sure to Read the linked section for more information.
:::

### hasTarget() <Badge text="changed in 2.0.0" type=warning />

`Wormhole.hasTarget(to)`
Expand All @@ -135,6 +141,11 @@ Wormhole.hasTarget('destination')
// => true/false
```

::: tip Server-Side Rendering
For the reasons layed out in [the section about SSR](../guide/SSR.md), this method will aleways return `false` during Server-Side Rendering.
Make sure to Read the linked section for more information.
:::

### hasContentFor() <Badge text="changed in 2.1.0" type=warning />

`Wormhole.hasContentFor(to)`
Expand All @@ -147,3 +158,8 @@ Example:
Wormhole.hasContentFor('destination')
// => true/false
```

::: tip Server-Side Rendering
For the reasons layed out in [the section about SSR](../guide/SSR.md), this method will aleways return `false` during Server-Side Rendering.
Make sure to Read the linked section for more information.
:::
31 changes: 31 additions & 0 deletions docs/guide/SSR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Server-Side Rendering (SSR)

When using [Vue's SSR capabilities](https://ssr.vuejs.org), portal-vue can't work reliably for a couple of reasons:

1. The internal Store (the [Wormhole](../api/wormhole.md)) that's caching vnodes and connecting `<portal>` components to their `<portal-target>` counterparts, is a singleton. As such, changes to the Wormhole persist between requests, leading to all sorts of problems.
2. In SSR, Vue renders the page directly to a string, there are not reactive updates applied. Consequently, a `<portal-target>` appearing before a `<portal>` will render an empty div on the server whereas it will render the sent content on the client, resulting in a hydration vdom mismatch error, while a `<portal-target>` _following_ a `<portal>` would technically work.

## Solutions

### Disabling the portal on the server

For the aforementioned reasons, starting with <Badge text="2.1.2" />, content won't be cached in the Wormhole anymore when on the server. Consequently, the HTML rendered by the server won't contain any DOM nodes in place of any `<portal-target>`

### Handling on the client

We want to display the `<portal-target>` content on the client, though. In order to prevent any hydration mismatches, we can use a _really_ tiny [component called `<no-ssr>`](https://github.com/egoist/vue-no-ssr), written by [@egoist](https://github.com/egoist), which can solve this problem.

We wrap oour `<portal-target>` elements in it, and it will prevent rendering on the server as well as on the client during hydration, preventing the error described above. Immediatly _after_ hyration, it will render the previously "hidden" content, so that the `<portal-target>` will render its content. Usually the user can hardly notice this as the update is near-immediate.

Example:

```html
<no-ssr>
<portal-target name="destination">
</no-ssr>

<!-- with placeholder text, usually not necessary -->
<no-ssr placeholder="Loading...">
<portal-target name="destination">
</no-ssr>
```
21 changes: 0 additions & 21 deletions docs/guide/caveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,3 @@ this.$nextTick().then(
```

the reason is that depending on the secnario, it _can_ take one tick for the content to be sent to the [Wormhole](../api/wormhole.md) (the middleman between `<Portal>` and `<PortalTarget>`), and another one to be picked up by the `<PortalTarget>`.

## Server-Side Rendering

When you use [Vue's SSR capabilities](https://ssr.vuejs.org), portal-vue can't work reliably because Vue renders the page directly to a string, there are not reactive updates applied. That means that a `<portal-target>` appearing before a `<portal>` will render an empty div on the server whereas it will render the sent content on the client, resulting in a hydration vdom mismatch error.

### Solution

[@egoist](https://github.com/egoist) has written a _really_ tiny [component called `<no-ssr>`](https://github.com/egoist/vue-no-ssr), which can solve this problem. You wrap your `<portal-target>` elements in it, and it will prevent rendering on the server as well as on the client during hydration, preventing the error described above. Immediatly after hyration, it will render the previously "hidden" content, so that the `<portal-target>` will render its content. Usually the user can hardly notice this as the update is near-immediate.

Example:

```html
<no-ssr>
<portal-target name="destination">
</no-ssr>

<!-- with placeholder text, usually not necessary -->
<no-ssr placeholder="Loading...">
<portal-target name="destination">
</no-ssr>
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "portal-vue",
"version": "2.1.1",
"version": "2.1.2",
"license": "MIT",
"repository": "https://github.com/LinusBorg/portal-vue",
"author": {
Expand Down
7 changes: 5 additions & 2 deletions src/components/wormhole.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Vue from 'vue'
import { freeze, stableSort } from '../utils'
import { freeze, inBrowser, stableSort } from '../utils'
import {
Transports,
Transport,
Expand All @@ -17,10 +17,11 @@ export const Wormhole = Vue.extend({
transports,
targets,
sources,
trackInstances: true,
trackInstances: inBrowser,
}),
methods: {
open(transport: TransportInput) {
if (!inBrowser) return
const { to, from, passengers, order = Infinity } = transport
if (!to || !from || !passengers) return

Expand Down Expand Up @@ -71,6 +72,7 @@ export const Wormhole = Vue.extend({
}
},
registerTarget(target: string, vm: Vue, force?: boolean): void {
if (!inBrowser) return
if (this.trackInstances && !force && this.targets[target]) {
console.warn(`[portal-vue]: Target ${target} already exists`)
}
Expand All @@ -80,6 +82,7 @@ export const Wormhole = Vue.extend({
this.$delete(this.targets, target)
},
registerSource(source: string, vm: Vue, force?: boolean): void {
if (!inBrowser) return
if (this.trackInstances && !force && this.sources[source]) {
console.warn(`[portal-vue]: source ${source} already exists`)
}
Expand Down
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { VNode } from 'vue'
import { Transport } from '../types'

export const inBrowser = typeof window !== 'undefined'

export function freeze<R>(item: R[]): ReadonlyArray<R> {
if (Array.isArray(item) || typeof item === 'object') {
return Object.freeze(item)
Expand Down
1 change: 1 addition & 0 deletions types/lib/utils/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { VNode } from 'vue';
import { Transport } from '../types';
export declare const inBrowser: boolean;
export declare function freeze<R>(item: R[]): ReadonlyArray<R>;
export declare function combinePassengers(transports: Transport[], slotProps?: {}): Array<VNode>;
export declare function stableSort<T>(array: T[], compareFn: Function): T[];
Expand Down

0 comments on commit 6521f47

Please sign in to comment.