Skip to content

Commit

Permalink
feat: useBroadcastChannel, refShared and useSharedRef (#104)
Browse files Browse the repository at this point in the history
* feat: useBroadcastChannel, refShared and useSharedRef

* add support flag

* WIP added leave event

* allow master to be assigned randomly if the current master goes off

* add better typescript to events

* improve master set when targets disconnect

* added broadcastChannel tests

* add tests

* Add documentation

* changelog

* cleanup
  • Loading branch information
pikax committed Jan 23, 2020
1 parent bf08555 commit ebf33e1
Show file tree
Hide file tree
Showing 25 changed files with 1,596 additions and 15 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

---

### Added

- [BroadcastChannel](https://pikax.me/vue-composable/composable/web/broadcastChannel) - reactive `BroadcastChannel API`
- [sharedRef](https://pikax.me/vue-composable/composable/misc/sharedRef.md) - cross-tab reactive `ref`

### Changed

- Axios: Allow calling exec with a string
Expand Down
47 changes: 47 additions & 0 deletions docs/.vuepress/components/BroadcastChannelExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div>
<p>
Supported: <b>{{ supported }}</b>
</p>
<p>
To test please open 2 or more tabs, press send and all the other tabs
should show the message
</p>

<p v-if="data">received: {{ data }}</p>

<div>
<input
v-model="message"
placeholder="Write a message"
@keydown.enter="submitMessage"
/>
<button @click="submitMessage">send</button>
</div>
</div>
</template>

<script>
import { useBroadcastChannel } from "vue-composable";
import { ref } from "@vue/composition-api";
export default {
name: "broadcast-channel-example",
setup() {
const { supported, data, send } = useBroadcastChannel("composable-example");
const message = ref("");
const submitMessage = () => {
send(message.value);
message.value = "";
};
return {
supported,
data,
message,
submitMessage
};
}
};
</script>
19 changes: 19 additions & 0 deletions docs/.vuepress/components/RefSharedExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<div>
<p>
To test please open 2 or more tabs and edit the input box
</p>
<input v-model="myRefVar" />
</div>
</template>

<script>
import { refShared } from "vue-composable";
export default {
name: "ref-shared-example",
setup() {
const myRefVar = refShared("Hello world");
return { myRefVar };
}
};
</script>
44 changes: 44 additions & 0 deletions docs/.vuepress/components/SharedRefExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div>
<p>Supported: {{ supported }}</p>
<p>
Mind: {{ mindDescription }} <button @click="changeMind">Change</button>
</p>
<p>IsMaster: {{ master }}</p>

<input v-model="data" :disabled="!editable" />

<p>targets: {{ targets.length }}</p>
</div>
</template>

<script>
import { useSharedRef } from "vue-composable";
import { computed } from "@vue/composition-api";
export default {
name: "shared-ref-example",
setup() {
const sharedRef = useSharedRef("test-shared-ref", 0);
const mindDescription = computed(() => {
switch (sharedRef.mind.value) {
case 0:
return "HIVE";
case 1:
return "MASTER";
}
});
const changeMind = () => {
s.setMind((s.mind.value + 1) % 2);
};
return {
...sharedRef,
mindDescription,
changeMind
};
}
};
</script>
3 changes: 3 additions & 0 deletions docs/.vuepress/components/SharedRefVideo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<video style="width:100%" src="/master-hive.mp4" controls />
</template>
6 changes: 4 additions & 2 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ module.exports = {
collapsable: false,
children: [
["composable/misc/matchMedia", "matchMedia"],
["composable/misc/breakpoint", "breakpoint"]
["composable/misc/breakpoint", "breakpoint"],
["composable/misc/sharedRef", "SharedRef"]
]
},
{
Expand Down Expand Up @@ -215,7 +216,8 @@ module.exports = {
["composable/web/networkInformation", "NetworkInformation"],
["composable/web/online", "Navigator.onLine"],
["composable/web/pageVisibility", "PageVisibilityAPI"],
["composable/web/language", "Language"]
["composable/web/language", "Language"],
["composable/web/broadcastChannel", "BroadcastChannel API"]
]
},
{
Expand Down
Binary file added docs/.vuepress/public/master-hive.mp4
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Check out the [examples folder](examples) or start hacking on [codesandbox](http

- [MatchMedia](composable/misc/matchMedia) - reactive `MatchMedia`
- [Breakpoint](composable/misc/breakpoint) - reactive `breakpoints` based on `window.innerWidth`
- [sharedRef](composable/misc/sharedRef) - cross-tab reactive `ref`

### Storage

Expand Down Expand Up @@ -84,6 +85,7 @@ Check out the [examples folder](examples) or start hacking on [codesandbox](http
- [Online](composable/web/online) - reactive `navigator.onLine` wrapper
- [PageVisibility](composable/web/pageVisibility) - reactive `Page Visibility API`
- [Language](composable/web/language) - reactive `NavigatorLanguage`
- [BroadcastChannel](composable/web/broadcastChannel) - reactive `BroadcastChannel API`

### External

Expand Down
231 changes: 231 additions & 0 deletions docs/composable/misc/sharedRef.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# useSharedRef

> Share the same value across multiple tabs (within the same origin) in the same browser.
::: warning

This relies on [BroadcastChannel API](./../web/broadcastChannel.md).
Safari doesn't support it.

:::

::: tip

You can use short version [refShared](#refshared)

:::

## Parameters

```js
import { useSharedRef } from "vue-composable";

const sharedRef = useSharedRef(name, defaultValue?);
```
| Parameters | Type | Required | Default | Description |
| ------------ | -------- | -------- | ----------- | ---------------------------- |
| name | `String` | `true` | | Name of the BroadcastChannel |
| defaultValue | `T` | false | `undefined` | Default value |
## State
The `useSharedRef` function exposes the following reactive state:
```js
import { useSharedRef } from "vue-composable";

const { supported, id, data, master, mind, editable, targets } = useSharedRef();
```
| State | Type | Description |
| --------- | --------------------------- | --------------------------------------------------------------------------------- |
| supported | `boolean` | Returns true if [BroadcastChannel API](./../web/broadcastChannel.md) is supported |
| id | `number` | Id of the reference |
| data | `Ref<T>` | Value |
| mind | `Ref<RefSharedMessageType>` | This describes on how this `ref` interacts with other tabs [check](#mind) |
| master | `Ref<boolean>` | If this instance is `master` instance |
| editable | `Ref<boolean>` | If this instance is editable |
| targets | `Ref<number[]>` | Connected `ref` |
## Methods
The `useSharedRef` function exposes the following methods:
```js
import { useSharedRef } from "vue-composable";

const { ping, setMind, addListener } = useSharedRef();
```
| Signature | Description |
| ------------------- | ---------------------------------------------- |
| `ping()` | sends a ping to listeners |
| `setMind(MindMode)` | sets all the listeners to the same mode |
| `addListener` | adds a callback to Broadcast Channel `message` |
## Information
### Mind
This is how the multiple tabs interact with each other, is possible to have two modes:
- **0**: Hive (_default_)
- **1**: Master
```ts
const { setMind } = useSharedRef();
// javascript
setMind(0); // set mind to HIVE
setMind(1); // set mind to MASTER

// typescript
import { SharedRefMind } from "vue-composable";
setMind(SharedRefMind.HIVE); // set mind to HIVE
setMind(SharedRefMind.MASTER); // set mind to MASTER
```
::: tip
When a `useSharedRef(id)` is called it will try to sync with other tabs, `setMind` only needs to set in one intance.
:::
#### HIVE MODE
This interaction allows the `ref` to be updated by everyone.
If you have 5 tabs open every tab is allowed to update and everyone will sync to the latest modified value.
#### MASTER MODE
This interaction only allows the `master` to send updates to the other tabs. In `master` mode if the reference is not master the `data` **value changes will be ignored**
::: tip
If you want to change the master instance, please call `setMind(1)` on the instance you want to control, this will update all of the other.
:::
::: warning
If the master instance gets destroyed (the component unmounted or tab closed) a new `master` will be assigned automatically (it will be the oldest instance)
:::
## Example
<ClientOnly>
<shared-ref-video/>
</ClientOnly>
<ClientOnly>
<shared-ref-example/>
</ClientOnly>
### Code
```vue
<template>
<div>
<p>Supported: {{ supported }}</p>
<p>
Mind: {{ mindDescription }} <button @click="changeMind">Change</button>
</p>
<p>IsMaster: {{ master }}</p>

<input v-model="data" :disabled="!editable" />

<p>targets: {{ targets.length }}</p>
</div>
</template>

<script>
import { useSharedRef } from "vue-composable";
import { computed } from "@vue/composition-api";

export default {
name: "shared-ref-example",
setup() {
const sharedRef = useSharedRef("test-shared-ref", 0);

const mindDescription = computed(() => {
switch (sharedRef.mind.value) {
case 0:
return "HIVE";
case 1:
return "MASTER";
}
});

const changeMind = () => {
s.setMind((s.mind.value + 1) % 2);
};

return {
...sharedRef,
mindDescription,
changeMind
};
}
};
</script>
```
## refShared
Short version, this uses [useSharedRef](#usesharedref) in HIVE mode.
### Parameters
```js
import { refShared } from "vue-composable";

const refShared = refShared(defaultValue?, id?);
```
| Parameters | Type | Required | Default | Description |
| ------------ | -------- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `T` | false | `undefined` | Default value |
| id | `String` | `false` | `undefined` | BroadcastChannel name, if not provided it will get be default `getCurrentInstance().$vnode.tag` this will allow have 1 shared reference per component |
### State
The `refShared` function returns a reactive `ref` variable:
```js
import { refShared } from "vue-composable";

const myShared = useSharedRef();
```
### Example
<ClientOnly>
<ref-shared-example/>
</ClientOnly>
#### Code
### Code
```vue
<template>
<div>
<p>
To test please open 2 or more tabs and edit the input box
</p>
<input v-model="myRefVar" />
</div>
</template>

<script>
import { refShared } from "vue-composable";
export default {
name: "ref-shared-example",
setup() {
const myRefVar = refShared("Hello world");
return { myRefVar };
}
};
</script>
```

0 comments on commit ebf33e1

Please sign in to comment.