Skip to content

Commit

Permalink
url-state-provider: use human readable url state
Browse files Browse the repository at this point in the history
  • Loading branch information
andypf committed Aug 24, 2023
1 parent 9815c47 commit dff6833
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 91 deletions.
2 changes: 1 addition & 1 deletion apps/greenhouse/src/hooks/useUrlState.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const useUrlState = () => {
try {
// get active app name from url state
const activeApp = state?.[GREENHOUSE_STATE_KEY]?.[ACTIVE_APPS_KEY]

if (activeApp) {
// add active app name and state
title += ` - ${activeApp}`
Expand Down
144 changes: 135 additions & 9 deletions libs/url-state-provider/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,134 @@
# URL State Provider
# URL State Provider: Streamlining State Management in the URL

Manage multiple states in the URL.
URL State Provider is your solution for simplifying the intricate task of managing multiple application states within the URL. Originally conceived for Micro Frontends (MFEs), this module empowers you to effortlessly navigate between diverse views while preserving the art of deep linking.

This module was originally developed for use in Micro Frontends (MFEs). In our projects we have had larger MFEs, each of which implemented several views. The navigation between these views should be managed with the help of the address bar in the browser. This should also enable deep links.
## Key Features

URL State Provider does nothing more than manage a global state and synchronize that state with the URL. Each application can use the `push` function, which requires an unique key, to save its own state in the global state. Whenever the global state changes, a specific search parameter in the URL is updated immediately. And vice versa, every change in the URL has an immediate effect on the global state.
- **Global State Synchronization:** Seamlessly synchronize your application's global state with the URL. URL State Provider offers a centralized state management mechanism, ensuring your data stays in perfect harmony with your application's navigation.

- **Push Function:** Empower your applications with the 'push' function, complete with a unique key. This allows each application to autonomously store and manage its own state within the global state, ensuring data integrity.

- **Immediate URL Updates:** Watch in amazement as every tweak in your global state triggers an instant update to a specific search parameter in the URL. Conversely, any alteration in the URL promptly influences the global state. It's bidirectional synchronization at its best.

By integrating URL State Provider, you unlock the ability to manage complex application states with ease, right from the heart of the URL. This not only enhances navigation but also makes deep linking a breeze within your Micro Frontends.

## Automatic Encoding Detection

URL State Provider goes the extra mile with its intelligent encoding detection mechanism, simplifying the choice between two encoding methods: human-readable encoding via the Json-URL library and efficient LZString encoding. The selection is made dynamically based on the length of the data being encoded.

- **Human-Readable Encoding:** When your data falls within a reasonable range for human readability, URL State Provider elegantly utilizes the Json-URL library for encoding. The result? URL parameters that are not only efficient but also user-friendly, making your URLs a joy to read.

- **LZString Encoding:** However, should your data transcend the bounds of human readability, fear not. URL State Provider is quick to adapt, seamlessly switching to LZString encoding. This method efficiently compresses your data, ensuring your URLs remain lean and optimized.

By automatically determining the most suitable encoding method, URL State Provider transforms URL management into a graceful dance, effortlessly balancing readability and efficiency.

## Table of Contents

- [Installation](#installation)
- [Usage](#usage)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Contributing](#contributing)
- [License](#license)

## Installation

You can install the URL State Provider library via npm or yarn:

```bash
npm install url-state-provider
# or
yarn add url-state-provider
```

## Usage

Low level
Here's a basic example of how to use URL State Provider in your JavaScript application:

```javascript
import urlStateProvider from "url-state-provider"

// get initial state for app1 from URL (initial page URL)
urlStateProvider.currentState("app1")

// add a new URL to the history object
urlStatusProvider.push("app1", { p: "/items" })
// replace last URL with a new one
urlStatusProvider.replace("app1", { p: "/items/new" })
```

## API Reference

The URL State Router library provides a set of methods and functions to help you manage and manipulate your application's state in the URL. Below is a reference guide to these methods:

### `URLStateRouter.registerConsumer(stateID)`

- **Description:** Registers a consumer for a specific state identified by `stateID`.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.
- **Returns:** An object with the following methods:
- `currentState()`: Get the current state for the registered `stateID`.
- `onChange(callback)`: Add a listener to be notified when the state changes.
- `onGlobalChange(callback)`: Add a listener to be notified when any state changes.
- `push(state, historyOptions)`: Push a new state to the URL, optionally providing history options.
- `replace(state, historyOptions)`: Replace the current state in the URL, optionally providing history options.

### `URLStateRouter.currentState(stateID)`

- **Description:** Get the current state for a specific `stateID`.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.
- **Returns:** The current state for the specified `stateID`.

### `URLStateRouter.onChange(stateID, callback)`

- **Description:** Add a listener to be notified when a specific state identified by `stateID` changes.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.
- `callback` (function): The function to be executed when the state changes.

### `URLStateRouter.onGlobalChange(callback)`

- **Description:** Add a listener to be notified when any state changes.
- **Parameters:**
- `callback` (function): The function to be executed when any state changes.

### `URLStateRouter.push(stateID, state, historyOptions)`

- **Description:** Push a new state to the URL for a specific `stateID`. The old and new states are merged.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.
- `state` (object): The new state to be pushed.
- `historyOptions` (object, optional): Options for the window.history, such as `state`, `title`, and `replace`.

### `URLStateRouter.replace(stateID, state, historyOptions)`

- **Description:** Replace the current state in the URL for a specific `stateID`. The old state is overwritten.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.
- `state` (object): The new state to be replaced.
- `historyOptions` (object, optional): Options for the window.history, such as `state`, `title`, and `replace`.

### `URLStateRouter.addOnChangeListener(stateID, listener)`

- **Description:** Add a listener for history changes for a specific `stateID`.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.
- `listener` (function): The function to be executed when the history changes.

### `URLStateRouter.removeOnChangeListener(stateID)`

- **Description:** Remove a listener for a specific `stateID`.
- **Parameters:**
- `stateID` (string): The key to identify the specific state in the search parameters.

---

Please make sure to adjust and expand this reference based on the specific details and usage of your library. Include descriptions, parameter explanations, and any additional context that helps users understand how to use each function effectively.

## Examples

### Low level

```js
import urlStateProvider from "url-state-provider"
Expand All @@ -33,15 +153,17 @@ urlStatusProvider.replace("app1", { p: "/items/new" })
urlStateProvider.removeOnChangeListener("app1")
```

High level
### High level

```js
import urlStateProvider from "url-state-provider"

var consumer2 = urlStateProvider.registerConsumer("app2")
const consumer1 = urlStateProvider.registerConsumer("app1")
const consumer2 = urlStateProvider.registerConsumer("app2")

var unregisterConsumer2 = consumer2.onChange((newState) =>
console.log(newState)
// Listen for changes in state resulting from navigation button clicks
const unregisterConsumer2 = consumer2.onChange((newState) =>
console.log("State changed due to navigation:", newState)
)

consumer2.currentState()
Expand All @@ -50,3 +172,7 @@ consumer2.replace({ p: "/items/new" })

unregisterConsumer2()
```

## License

URL State Provider is licensed under the [MIT License](LICENSE).
5 changes: 3 additions & 2 deletions libs/url-state-provider/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "url-state-provider",
"version": "1.2.5",
"version": "1.3.0",
"description": "Save several paths in the url. This makes it possible to manage the status of several apps such as micro frontends with the help of URL.",
"source": "src/index.js",
"main": "build/cjs/index.js",
Expand All @@ -22,7 +22,8 @@
"lz-string": "^1.4.4",
"rollup": "^3.4.0",
"rollup-plugin-analyzer": "^4.0.0",
"rollup-plugin-delete": "^2.0.0"
"rollup-plugin-delete": "^2.0.0",
"@jsonurl/jsonurl": "^1.1.7"
},
"babel": {
"presets": [
Expand Down
33 changes: 28 additions & 5 deletions libs/url-state-provider/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import LZString from "lz-string"
import JsonURL from "@jsonurl/jsonurl"

var SEARCH_KEY = "__s"
var URL_REGEX = new RegExp("[?&]" + SEARCH_KEY + "=([^&#]*)")
Expand All @@ -10,29 +11,49 @@ var onHistoryChangeListeners = {}
var onGlobalChangeListeners = []

/**
* Encode json data using lz-string
* Encode json data using json-url or lz-string. It automatically detects the best encoding.
* @param {JSON} json data to be encoded
* @param {Object} options options for the encoding. Possible values: mode: "auto" or "humanize"
* @returns encoded string
*/
function encode(json) {
function encode(json, options = {}) {
try {
return LZString.compressToEncodedURIComponent(JSON.stringify(json))
let urlState = JsonURL.stringify(json, {
AQF: true,
//impliedStringLiterals: true,
ignoreNullArrayMembers: true,
ignoreNullObjectMembers: true,
})
if (options?.mode === "humanize") return urlState

if (urlState.length > 1800) {
urlState = LZString.compressToEncodedURIComponent(JSON.stringify(json))
}
//return LZString.compressToEncodedURIComponent(JSON.stringify(json))
return urlState
} catch (e) {
console.warn("URL State Router: Could not encode data", data)
return ""
}
}

/**
* Decode using lz-string
* Decode using json-url or lz-string. It automatically detects the encoding.
* @param {string} string to be decoded
* @returns json
*/
function decode(string) {
try {
// try to decode as jsonurl
let json = JsonURL.parse(string, { AQF: true })

// if parsed value is an object, return it
if (json && typeof json === "object") return json

// try to decode as lz-string
return JSON.parse(LZString.decompressFromEncodedURIComponent(string))
} catch (e) {
console.warn("URL State Router: Could not decode string", string, e)
console.warn("URL State Router: Could not decode string: ", string, e)
return {}
}
}
Expand Down Expand Up @@ -279,4 +300,6 @@ export {
stateToURL,
stateToQueryParam,
onGlobalChange,
decode,
encode,
}
Loading

0 comments on commit dff6833

Please sign in to comment.