statepod 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Alexander Tkačenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # statepod
2
+
3
+ Vanilla TS/JS state management for sharing data across decoupled parts of the code and routing. Routing is essentially shared state management, too, with the shared data being the URL.
4
+
5
+ This package exposes the following classes:
6
+
7
+ ```
8
+ EventEmitter ──► State ──► PersistentState
9
+
10
+ └────► URLState ──► Route
11
+ ```
12
+
13
+ Roughly, their purpose boils down to the following:
14
+
15
+ - `EventEmitter` is for triggering actions without tightly coupling the interacting components
16
+ - `State` is `EventEmitter` that stores data and emits an event when the data gets updated, it's for dynamic data sharing without tight coupling
17
+ - `PersistentState` is `State` that syncs its data to the browser storage and restores it on page reload
18
+ - `URLState` is `State` that stores the URL + syncs with the browser's URL in a SPA fashion
19
+ - `Route` is `URLState` + native-like APIs for SPA navigation and an API for URL matching
20
+
21
+ Contents: [State](#state) · [PersistentState](#persistentstate) · [Route](#route) · [Annotated examples](#annotated-examples) · [Integrations](#integrations)
22
+
23
+ ## `State`
24
+
25
+ A thin data container for dynamic data sharing without tight coupling.
26
+
27
+ ```js
28
+ import { State } from "statepod";
29
+
30
+ const counterState = new State(42);
31
+
32
+ document.querySelector("button").addEventListener("click", () => {
33
+ counterState.setValue((value) => value + 1);
34
+ });
35
+
36
+ counterState.on("set", ({ current }) => {
37
+ document.querySelector("output").textContent = String(current);
38
+ });
39
+ ```
40
+
41
+ In this example, a button changes a counter value and an `<output>` element shows the updating value. Both elements are only aware of the shared counter state, but not of each other.
42
+
43
+ A `"set"` event callback is called each time the state value changes and immediately when the callback is added. Subscribe to the `"update"` event to have the callback respond only to the subsequent state changes without the immediate invocation.
44
+
45
+ ## `PersistentState`
46
+
47
+ A variety of `State` that syncs its data to the browser storage and restores it on page reload. Otherwise, almost identical to `State` in usage.
48
+
49
+ ```diff
50
+ - import { State } from "statepod";
51
+ + import { PersistentState } from "statepod";
52
+
53
+ - const counterState = new State(42);
54
+ + const counterState = new PersistentState(42, { key: "counter" });
55
+
56
+ document.querySelector("button").addEventListener("click", () => {
57
+ counterState.setValue((value) => value + 1);
58
+ });
59
+
60
+ counterState.on("set", ({ current }) => {
61
+ document.querySelector("output").textContent = String(current);
62
+ });
63
+ ```
64
+
65
+ By default, `PersistentState` stores its data at the specified `key` in `localStorage` and transforms the data with `JSON.stringify()` and `JSON.parse()`. Switch to `sessionStorage` by setting `options.session` to `true` in `new PersistentState(value, options)`. Set custom `serialize()` and `deserialize()` in `options` to override the default data transforms used with the browser storage. Alternatively, use custom `{ read(), write()? }` as `options` to set up custom interaction with an external storage.
66
+
67
+ Instances of `PersistentState` automatically sync their values with the browser storage when created and updated. At other times, call `.emit("sync")` on a `PersistentState` instance to sync its value from the browser storage when needed.
68
+
69
+ ## `Route`
70
+
71
+ Stores the URL, exposes a native-like API for SPA navigation and an API for URL matching.
72
+
73
+ ```js
74
+ import { Route } from "statepod";
75
+
76
+ const route = new Route();
77
+ ```
78
+
79
+ Navigate to other URLs in a SPA fashion similarly to the native APIs:
80
+
81
+ ```js
82
+ route.href = "/intro";
83
+ route.assign("/intro");
84
+ route.replace("/intro");
85
+ ```
86
+
87
+ Or in a more fine-grained manner:
88
+
89
+ ```js
90
+ route.navigate({ href: "/intro", history: "replace", scroll: "off" });
91
+ ```
92
+
93
+ Check the current URL value like a regular `string` with `route.href`:
94
+
95
+ ```js
96
+ route.href === "/intro";
97
+ route.href.startsWith("/sections/");
98
+ /^\/sections\/\d+\/?/.test(route.href);
99
+ ```
100
+
101
+ Or, alternatively, with `route.at(url)` returning `true` if the current URL matches `url`, and `false` otherwise:
102
+
103
+ ```js
104
+ route.at("/intro"); // `true` at "/intro", `false` otherwise
105
+ route.at(/^\/sections\//); // `true` at "/sections/*"
106
+ ```
107
+
108
+ Or with the extended form `route.at(url, x, y?)`, which is similar to the ternary conditional operator `atURL ? x : y`:
109
+
110
+ ```js
111
+ document.querySelector("header").className = route.at("/", "full", "compact");
112
+ // at "/" ? then "full" : otherwise "compact"
113
+ ```
114
+
115
+ Use `route.at(url, x, y?)` with dynamic values that require values from the URL pattern's capturing groups:
116
+
117
+ ```js
118
+ document.querySelector("h1").textContent = route.at(
119
+ /^\/sections\/(?<id>\d+)\/?/,
120
+ ({ params }) => `Section ${params.id}`,
121
+ );
122
+ // at "/sections/:id", "Section <id>" (otherwise `undefined`)
123
+ ```
124
+
125
+ Or, alternatively, with a string URL pattern:
126
+
127
+ ```js
128
+ import { url } from "url-shape";
129
+
130
+ document.querySelector("h1").textContent = route.at(
131
+ url("/sections/:id"),
132
+ ({ params }) => `Section ${params.id}`,
133
+ );
134
+ // at "/sections/:id", "Section <id>" (otherwise `undefined`)
135
+ ```
136
+
137
+ Use a URL builder like the one from `url-shape` also in conjunction with a URL schema to set up type-safe routes ([example](https://codesandbox.io/p/sandbox/qg7qg3?file=%2Fsrc%2Findex.ts)).
138
+
139
+ Enable SPA navigation with HTML links inside the specified container (or the entire `document`) without any changes to the HTML:
140
+
141
+ ```js
142
+ route.observe(document);
143
+ ```
144
+
145
+ Tweak the links' navigation behavior by adding a relevant combination of the optional `data-` attributes (corresponding to the `route.navigate()` options):
146
+
147
+ ```html
148
+ <a href="/intro">Intro</a>
149
+ <a href="/intro" data-history="replace">Intro</a>
150
+ <a href="/intro" data-scroll="off">Intro</a>
151
+ <a href="/intro" data-spa="off">Intro</a>
152
+ ```
153
+
154
+ Links covered by `route.observe(container, selector?)` also automatically receive the `data-active="true"` attribute whenever their `href` attribute matches the current URL, which can be used for additional styling. If links are added to the DOM asynchronously, call `route.emit("ready")` when the rendering is complete to mark active links.
155
+
156
+ Define what should be done when the URL changes:
157
+
158
+ ```js
159
+ route.on("navigationcomplete", ({ href }) => {
160
+ renderContent();
161
+ });
162
+ ```
163
+
164
+ Define what should be done before the URL changes (in a way effectively similar to routing middleware):
165
+
166
+ ```js
167
+ route.on("navigationstart", ({ href }) => {
168
+ if (hasUnsavedInput)
169
+ return false; // Quit the navigation, prevent the current URL change
170
+
171
+ if (href === "/") {
172
+ route.href = "/intro"; // SPA redirection
173
+ return false;
174
+ }
175
+ });
176
+ ```
177
+
178
+ ## Annotated examples
179
+
180
+ - [Shared state](https://codesandbox.io/p/sandbox/lqt3z2?file=%252Fsrc%252Findex.ts), counter app, State
181
+ - [Shared form input state](https://codesandbox.io/p/sandbox/4q7f99?file=%252Fsrc%252Findex.ts), simple form, State
182
+ - [Persistent shared state](https://codesandbox.io/p/sandbox/c9gt3r?file=%252Fsrc%252Findex.ts), counter app, PersistentState
183
+ - [URL-based rendering](https://codesandbox.io/p/sandbox/kt6m5l?file=%252Fsrc%252Findex.ts), Route
184
+ - [Type-safe URL-based rendering](https://codesandbox.io/p/sandbox/qg7qg3?file=%2Fsrc%2Findex.ts), Route, url-shape, zod
185
+ - [SPA redirection](https://codesandbox.io/p/sandbox/rpl3gh?file=%252Fsrc%252Findex.ts), Route
186
+
187
+ Find also the code of these examples in the repo's [`tests`](https://github.com/axtk/statepod/tree/main/tests) directory.
188
+
189
+ ## Integrations
190
+
191
+ [`react-statepod`](https://www.npmjs.com/package/react-statepod)