@coherent.js/client 1.1.1 → 2.0.0-rc.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/README.md +126 -66
- package/dist/chunk-EAOAAY2X.js +1550 -0
- package/dist/chunk-EAOAAY2X.js.map +7 -0
- package/dist/{chunk-N36BSMJU.js → chunk-UYL3RRRC.js} +134 -32
- package/dist/chunk-UYL3RRRC.js.map +7 -0
- package/dist/events/index.js +1 -1
- package/dist/hmr.js +34 -4
- package/dist/hmr.js.map +3 -3
- package/dist/index.js +598 -1732
- package/dist/index.js.map +4 -4
- package/dist/router.js +209 -31
- package/dist/router.js.map +2 -2
- package/package.json +4 -1
- package/types/hmr.d.ts +32 -1
- package/types/index.d.ts +258 -701
- package/types/router.d.ts +34 -11
- package/dist/chunk-N36BSMJU.js.map +0 -7
package/README.md
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@coherent.js/client)
|
|
4
4
|
[](../../LICENSE)
|
|
5
|
-
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
Client-side hydration and HMR
|
|
7
|
+
Client-side hydration, event delegation, routing and HMR for Coherent.js
|
|
8
|
+
applications.
|
|
8
9
|
|
|
9
|
-
- ESM-only
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
10
|
+
- ESM-only
|
|
11
|
+
- Hydrates HTML rendered by `@coherent.js/core` with the same component
|
|
12
|
+
- Document-level event delegation that survives re-renders
|
|
13
|
+
- DOM patching on `setState()` / `rerender()`
|
|
14
|
+
- Router with `:param` patterns and browser history
|
|
15
|
+
- HMR client for development
|
|
13
16
|
|
|
14
17
|
For a high-level overview and repository-wide instructions, see the root README: ../../README.md
|
|
15
18
|
|
|
@@ -19,103 +22,160 @@ For a high-level overview and repository-wide instructions, see the root README:
|
|
|
19
22
|
pnpm add @coherent.js/client
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
- Node.js >= 20
|
|
24
|
-
- ESM module system
|
|
25
|
+
## Entry points
|
|
25
26
|
|
|
27
|
+
| Import | Contents |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `@coherent.js/client` | `hydrate`, event delegation, state serialization, mismatch detection, HMR client |
|
|
30
|
+
| `@coherent.js/client/events` | `EventDelegation`, `eventDelegation`, `HandlerRegistry`, `handlerRegistry`, `wrapEvent` |
|
|
31
|
+
| `@coherent.js/client/router` | `createRouter`, `router` |
|
|
32
|
+
| `@coherent.js/client/hmr` | The HMR client API on its own (no side effects on import) |
|
|
26
33
|
|
|
27
|
-
##
|
|
34
|
+
## Hydration
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
Render on the server with `@coherent.js/core`, then hydrate the same component
|
|
37
|
+
on the client:
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
```js
|
|
40
|
+
import { hydrate } from '@coherent.js/client';
|
|
41
|
+
|
|
42
|
+
function Counter({ count = 0 }) {
|
|
43
|
+
return {
|
|
44
|
+
div: {
|
|
45
|
+
className: 'counter',
|
|
46
|
+
children: [
|
|
47
|
+
{ span: { text: `Count: ${count}` } },
|
|
48
|
+
{
|
|
49
|
+
button: {
|
|
50
|
+
text: '+1',
|
|
51
|
+
onClick: (event) => event.setState({ count: event.state.count + 1 })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const app = hydrate(Counter, document.querySelector('.counter'), {
|
|
60
|
+
initialState: { count: 0 } // defaults to the container's data-state attribute
|
|
61
|
+
});
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
63
|
+
app.setState({ count: 5 }); // patches the DOM
|
|
64
|
+
app.getState(); // { count: 5 }
|
|
65
|
+
app.rerender({ label: 'x' }); // re-render with extra props
|
|
66
|
+
app.unmount(); // releases handlers; later setState() does nothing
|
|
67
|
+
```
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
`hydrate(component, container, options)` options:
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
| Option | Default | |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `initialState` | `data-state` of the container | State to hydrate with |
|
|
74
|
+
| `props` | `{}` | Extra props passed to the component |
|
|
75
|
+
| `detectMismatch` | on in development (`process.env.NODE_ENV === 'development'`), or when `strict`/`onMismatch` is set | Compare the server DOM with the component's output |
|
|
76
|
+
| `strict` | `false` | Throw on mismatch instead of warning |
|
|
77
|
+
| `onMismatch` | — | Receive the mismatches instead of the console warning |
|
|
43
78
|
|
|
44
|
-
|
|
45
|
-
## Quick start
|
|
79
|
+
Hydrating the same container again replaces the previous hydration.
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
### Event handlers
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
`on*` props become delegated handlers: `onClick` → `click`, `onDoubleClick` →
|
|
84
|
+
`dblclick`, `onPointerDown` → `pointerdown`, and so on — any DOM event works.
|
|
85
|
+
A handler receives a wrapped event:
|
|
52
86
|
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
```js
|
|
88
|
+
onClick: (event) => {
|
|
89
|
+
event.preventDefault(); // works: listeners are not passive
|
|
90
|
+
event.stopPropagation(); // stops ancestor handlers
|
|
91
|
+
event.originalEvent; // the native event
|
|
92
|
+
event.target; // the element carrying this handler
|
|
93
|
+
event.state; // component state when the event fired
|
|
94
|
+
event.setState({ open: true });
|
|
95
|
+
event.props; // props the component rendered with
|
|
96
|
+
}
|
|
55
97
|
```
|
|
56
98
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
Handlers run from the target outwards through every ancestor that has one,
|
|
100
|
+
like native bubbling. Non-bubbling events (`mouseenter`, `load`, ...) only run
|
|
101
|
+
the handler on their own element. Scroll-blocking events (`touchstart`,
|
|
102
|
+
`touchmove`, `wheel`, `scroll`) are delegated passively, so `preventDefault()`
|
|
103
|
+
has no effect on them.
|
|
60
104
|
|
|
61
|
-
|
|
62
|
-
autoHydrate();
|
|
63
|
-
});
|
|
64
|
-
```
|
|
105
|
+
### Re-rendering
|
|
65
106
|
|
|
66
|
-
|
|
107
|
+
`setState()` and `rerender()` diff the previous output against the new one:
|
|
108
|
+
children are added, removed or replaced (matched by `key` when every sibling
|
|
109
|
+
has one), attributes follow the server's rules (`style` objects, function
|
|
110
|
+
values, `true`/`false`), `html` content is updated, and `value`, `checked` and
|
|
111
|
+
`selected` are written to the element's properties too, so they are
|
|
112
|
+
controlled by state.
|
|
67
113
|
|
|
68
|
-
|
|
114
|
+
### State serialization and mismatches
|
|
69
115
|
|
|
70
116
|
```js
|
|
71
|
-
import {
|
|
117
|
+
import { serializeState, extractState, detectMismatch, reportMismatches } from '@coherent.js/client';
|
|
72
118
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
119
|
+
serializeState({ count: 1 }); // base64 string for data-state (null if empty)
|
|
120
|
+
extractState(element); // parsed data-state, or null
|
|
121
|
+
reportMismatches(detectMismatch(element, Counter({ count: 1 })), { componentName: 'Counter' });
|
|
76
122
|
```
|
|
77
123
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
124
|
+
## Router
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { createRouter } from '@coherent.js/client/router';
|
|
81
128
|
|
|
82
|
-
|
|
129
|
+
const router = createRouter({ base: '/app' }); // or { mode: 'hash' }
|
|
83
130
|
|
|
84
|
-
|
|
85
|
-
|
|
131
|
+
router.addRoute('/', { component: () => Home }); // a function is a loader
|
|
132
|
+
router.addRoute('/users/:id', {
|
|
133
|
+
component: () => import('./UserPage.js'), // lazy: resolves to the module
|
|
134
|
+
beforeEnter: (to) => to.params.id !== 'blocked' // false cancels
|
|
86
135
|
});
|
|
136
|
+
|
|
137
|
+
await router.start(); // resolve the current URL, follow back/forward,
|
|
138
|
+
// intercept clicks on links to registered routes
|
|
139
|
+
await router.push('/users/42?tab=posts');
|
|
140
|
+
router.getCurrentRoute(); // { path: '/users/42', params: { id: '42' }, query: { tab: 'posts' }, component, ... }
|
|
141
|
+
router.back();
|
|
142
|
+
router.stop();
|
|
87
143
|
```
|
|
88
144
|
|
|
89
|
-
|
|
145
|
+
The router resolves routes and tracks the current one; rendering the matched
|
|
146
|
+
component is up to the application. A function `component` is called once,
|
|
147
|
+
without arguments, and its awaited result becomes the route's component, so
|
|
148
|
+
wrap component functions: `component: () => Home`. The last navigation wins: a slow lazy
|
|
149
|
+
route that resolves after a later `push()` is dropped.
|
|
90
150
|
|
|
91
|
-
|
|
151
|
+
## HMR
|
|
92
152
|
|
|
93
153
|
```js
|
|
94
|
-
import {
|
|
154
|
+
import { hmrClient, createHotContext } from '@coherent.js/client';
|
|
155
|
+
|
|
156
|
+
hmrClient.initialize(); // connects to the dev server's WebSocket
|
|
95
157
|
|
|
96
|
-
|
|
97
|
-
|
|
158
|
+
const hot = createHotContext(import.meta.url);
|
|
159
|
+
hot.accept((newModule) => { /* apply the update */ });
|
|
160
|
+
hot.dispose((data) => { data.saved = currentState; });
|
|
98
161
|
```
|
|
99
162
|
|
|
100
|
-
|
|
163
|
+
A changed module without `accept()` reloads the page. Form values (including
|
|
164
|
+
radio groups) and scroll positions are preserved across updates; timers,
|
|
165
|
+
listeners and fetches created through `cleanupTracker.createContext(id)` are
|
|
166
|
+
released when the module is replaced.
|
|
101
167
|
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
pnpm --filter @coherent.js/client run test
|
|
105
|
-
```
|
|
168
|
+
## Notes on testing
|
|
106
169
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```
|
|
170
|
+
The package's own tests run in Node against a small DOM (see
|
|
171
|
+
`packages/client/test/helpers/dom.js`): it parses server HTML, dispatches
|
|
172
|
+
events with capture and bubbling, and models form properties.
|
|
111
173
|
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
pnpm --filter @coherent.js/client run typecheck
|
|
115
|
-
```
|
|
174
|
+
## Development
|
|
116
175
|
|
|
117
|
-
Build:
|
|
118
176
|
```bash
|
|
177
|
+
pnpm --filter @coherent.js/client run test
|
|
178
|
+
pnpm --filter @coherent.js/client run typecheck
|
|
119
179
|
pnpm --filter @coherent.js/client run build
|
|
120
180
|
```
|
|
121
181
|
|