Skip to content

Commit

Permalink
Merge pull request #46 from podium-lib/podium-store-bridge-browser
Browse files Browse the repository at this point in the history
docs: the bridge, browser, and store that's WIP
  • Loading branch information
wkillerud authored Jun 20, 2024
2 parents cff736e + bbf41d4 commit a28b357
Show file tree
Hide file tree
Showing 8 changed files with 501 additions and 18 deletions.
115 changes: 115 additions & 0 deletions docs/api/bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: "@podium/bridge"
---

This package is a bridge designed to pass [JSON-RPC 2.0](https://www.jsonrpc.org/specification) messages between a web application and a native web view.

## Usage

To install:

```sh
npm install @podium/bridge
```

Import the bridge in your client-side bundle:

```js
import "@podium/bridge";
```

You should probably send messages via [@podium/browser]. That said, the bridge is available on `window['@podium'].bridge`.

```js
/** @type {import("@podium/bridge").PodiumBridge} */
const bridge = window["@podium"].bridge;

// You can listen for incoming messages, which can either be RpcRequest or RpcResponse
bridge.on("global/authentication", (message) => {
const request =
/** @type {import("@podium/bridge").RpcRequest<{ token?: string }>} */ (
message
);

if (typeof request.token === "string") {
// logged in
} else {
// logged out
}
});

// You can trigger notifications (one-way messages)
bridge.notification({
method: "global/authentication",
params: { token: null },
});

// And you can call methods and await the response
/** @type {import("@podium/bridge").RpcResponse<{ c: string }>} */
const response = await bridge.call({
method: "document/native-feature",
params: { a: "foo", b: "bar" },
});
```

## API

### `bridge.on`

Add a listener for incoming messages for a given method name.

```js
import "@podium/bridge";

/** @type {import("@podium/bridge").PodiumBridge} */
const bridge = window["@podium"].bridge;

bridge.on("global/authentication", (message) => {
const request =
/** @type {import("@podium/bridge").RpcRequest<{ token?: string }>} */ (
message
);

if (typeof request.token === "string") {
// logged in
} else {
// logged out
}
});
```

### `bridge.notification`

Send a [notification](https://www.jsonrpc.org/specification#notification) (one-way message).

```js
import "@podium/bridge";

/** @type {import("@podium/bridge").PodiumBridge} */
const bridge = window["@podium"].bridge;

bridge.notification({
method: "global/authentication",
params: { token: null },
});
```

### `bridge.call`

Send a [request](https://www.jsonrpc.org/specification#request_object) and await a [response](https://www.jsonrpc.org/specification#response_object).

```js
import "@podium/bridge";

/** @type {import("@podium/bridge").PodiumBridge} */
const bridge = window["@podium"].bridge;

/** @type {import("@podium/bridge").RpcResponse<{ c: string }>} */
const response = await bridge.call({
method: "document/native-feature",
params: { a: "foo", b: "bar" },
});
```

[@podium/browser]: /docs/api/browser
[@podium/store]: /docs/api/store
15 changes: 9 additions & 6 deletions docs/api/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ title: "@podium/browser"
---

The `@podium/browser` module is a client-side library designed to simplify communication between a podlet and the layout, and between podlets.
The module also supports applications running in a [hybrid application](/docs/guides/hybrid#client-side-communcication) when used with [@podium/bridge](/docs/api/bridge).

For an API designed as reactive state, see [@podium/store](/docs/api/store).

## Installation

Expand Down Expand Up @@ -55,11 +58,11 @@ Publish an event for a channel and topic combination. Returns the event object p

This method takes the following arguments:

| option | default | type | required | details |
| ------- | ------- | -------- | -------- | ------------------------- |
| channel | `null` | `string` | `true` | Name of the channel |
| topic | `null` | `string` | `true` | Name of the topic |
| payload | `null` | any | `false` | The payload for the event |
| option | default | type | required | details |
| ------- | ------- | -------- | -------- | ------------------------------------------------------------------------------- |
| channel | `null` | `string` | `true` | Name of the channel. Podium reserves `system` and `view` for built-in features. |
| topic | `null` | `string` | `true` | Name of the topic. |
| payload | `null` | any | `false` | The payload for the event. |

Examples:

Expand All @@ -77,7 +80,7 @@ This method takes the following arguments:

| option | default | type | required | details |
| -------- | ------- | ---------- | -------- | --------------------------------------------------------- |
| channel | `null` | `string` | `true` | Name of the channel |
| channel | `null` | `string` | `true` | Name of the channel. |
| topic | `null` | `string` | `true` | Name of the topic |
| callback | `null` | `Function` | `true` | Callback function to be invoked. Receives an event object |

Expand Down
158 changes: 158 additions & 0 deletions docs/api/store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
title: "@podium/store"
---

This is a client-side library that provides a reactive data store using [nanostores](https://github.com/stores/stores) on top of [@podium/browser](https://github.com/podium-lib/browser)'s `MessageBus`. It includes some ready-made stores and helpers for you to make reactive stores for your own events.

By using reactive state backed by `MessageBus` you can seamlessly share state between different parts of a Podium application. If a podlet changes the value of shared state, all other podlets using that value can update.

<!-- TODO: look into an existing nanostores plugin so we make something that fits the ecosystem -->

## Usage

To install:

```sh
npm install @podium/store
```

Use an [included store](#included-stores) in your client-side application:

```js
// store/user.js
import { $authentication } from "@podium/store";
import { computed } from "nanostores";

// You can optionally make a computed value based on the included store
export const $loggedIn = computed($authentication, (authentication) =>
Boolean(authentication.token)
);

// Colocating actions with the store makes it easier to
// see what can trigger updates.
export function logIn(token) {
$authentication.set({ token });
}

export function logOut() {
$authentication.set({ token: null });
}
```

Use the reactive store to do minimal updates of your UI when state changes. Nanostores supports multiple view frameworks:

- [React and Preact](https://github.com/stores/stores?tab=readme-ov-file#react--preact)
- [Lit](https://github.com/stores/stores?tab=readme-ov-file#lit)
- [Vue](https://github.com/stores/stores?tab=readme-ov-file#vue)
- [Vanilla JS](https://github.com/stores/stores?tab=readme-ov-file#vanilla-js)

This example shows a React component:

```js
// components/user.jsx
import { useStore } from "@nanostores/react";
import { $loggedIn } from "../stores/user.js";

export const User = () => {
const loggedIn = useStore($loggedIn);
return <p>{loggedIn ? "Welcome!" : "Please log in"}</p>;
};
```

This is the same component in Lit:

```js
// components/user.js
import { StoreController } from "@nanostores/lit";
import { $loggedIn } from "../stores/user.js";

class User extends LitElement {
loggedInController = new StoreController(this, $loggedIn);

render() {
return html`<p>
${this.loggedInController.value ? "Welcome!" : "Please log in"}
</p>`;
}
}

customElements.define("a-user", User);
```

### Create your own reactive state

By using the [included helper](#mapchannel-topic-initialvalue) you can make your reactive state sync between the different parts of a Podium application.

## API

### `$authentication`

Type: [`map`](https://github.com/stores/stores?tab=readme-ov-file#maps)

```js
import { $authentication } from "@podium/store";
```

### `atom(channel, topic, initialValue)`

Create your own [`atom`](https://github.com/nanostores/nanostores?tab=readme-ov-file#atoms) that syncs between parts of a Podium application using the [MessageBus](https://github.com/podium-lib/browser).

This method requires the following arguments:

| option | type | details |
| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| channel | `string` | Name of the channel |
| topic | `string` | Name of the topic |
| payload | `object` | The initial value. Replaced if [`peek(channel, topic)`](https://github.com/podium-lib/browser?tab=readme-ov-file#peekchannel-topic) returns a value. |

```js
import { atom } from "@podium/store";

const $reminders = atom("reminders", "list", []);
```

### `map(channel, topic, initialValue)`

Create your own [`map`](https://github.com/nanostores/nanostores?tab=readme-ov-file#maps) that syncs between parts of a Podium application using the [MessageBus](https://github.com/podium-lib/browser).

This method requires the following arguments:

| option | type | details |
| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| channel | `string` | Name of the channel |
| topic | `string` | Name of the topic |
| payload | `object` | The initial value. Replaced if [`peek(channel, topic)`](https://github.com/podium-lib/browser?tab=readme-ov-file#peekchannel-topic) returns a value. |

```js
import { map } from "@podium/store";

const $user = map("user", "profile", { displayName: "foobar" });
```

### `deepMap(channel, topic, initialValue)`

Create your own [`deepMap`](https://github.com/nanostores/nanostores?tab=readme-ov-file#deep-maps) that syncs between parts of a Podium application using the [MessageBus](https://github.com/podium-lib/browser).

This method requires the following arguments:

| option | type | details |
| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| channel | `string` | Name of the channel |
| topic | `string` | Name of the topic |
| payload | `object` | The initial value. Replaced if [`peek(channel, topic)`](https://github.com/podium-lib/browser?tab=readme-ov-file#peekchannel-topic) returns a value. |

```js
import { deepMap, listenKeys } from "@podium/store";

export const $profile = deepMap({
hobbies: [
{
name: "woodworking",
friends: [{ id: 123, name: "Ron Swanson" }],
},
],
skills: [["Carpentry", "Sanding"], ["Varnishing"]],
});

listenKeys($profile, ["hobbies[0].friends[0].name", "skills[0][0]"]);
```
9 changes: 9 additions & 0 deletions docs/guides/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,12 @@ Using the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_comp
With `podlet.css()` the end result is a `<link />` tag in the HTML document's `<head />`. If your podlet's content renders inside a shadow DOM that CSS won't be able to reach the podlet.

With a declarative shadow DOM you have to include your own `<link />` to the CSS from inside the shadow DOM.

### Islands architecture

Podium works well with [islands architecture](https://jasonformat.com/islands-architecture/) where interactivity on the client is handled by small, isolated applications.

Especially when building your layout:

- Consider how JavaScript libraries you use handle external content (external in the sense that it is not generated by your library).
- Be mindful of how much of the document your JavaScript library hydrates.
Loading

0 comments on commit a28b357

Please sign in to comment.