@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 CHANGED
@@ -2,14 +2,17 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@coherent.js/client.svg)](https://www.npmjs.com/package/@coherent.js/client)
4
4
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
5
- [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
5
+ [![Node >= 22.12](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org)
6
6
 
7
- Client-side hydration and HMR utilities for Coherent.js applications.
7
+ Client-side hydration, event delegation, routing and HMR for Coherent.js
8
+ applications.
8
9
 
9
- - ESM-only, Node 20+
10
- - Progressive enhancement for server-rendered HTML
11
- - Lightweight event system and instance lifecycle helpers
12
- - Optional HMR support for dev workflows
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
- Requirements:
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
- ## Exports
34
+ ## Hydration
28
35
 
29
- Client-side hydration and HMR utilities
36
+ Render on the server with `@coherent.js/core`, then hydrate the same component
37
+ on the client:
30
38
 
31
- ### Modular Imports (Tree-Shakable)
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
- - Hydration utilities: `@coherent.js/client`
34
- - Client router: `@coherent.js/client/router`
35
- - HMR support: `@coherent.js/client/hmr`
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
- ### Example Usage
69
+ `hydrate(component, container, options)` options:
38
70
 
39
- ```javascript
40
- import { hydrateComponent } from '@coherent.js/client';
41
- import { createClientRouter } from '@coherent.js/client/router';
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
- > **Note**: All exports are tree-shakable. Import only what you need for optimal bundle size.
45
- ## Quick start
79
+ Hydrating the same container again replaces the previous hydration.
46
80
 
47
- The client package pairs with server-rendered HTML produced by `@coherent.js/core`.
81
+ ### Event handlers
48
82
 
49
- JavaScript (ESM):
50
- ```js
51
- import { autoHydrate } from '@coherent.js/client';
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
- // Hydrate elements that were marked as hydratable on the server
54
- autoHydrate();
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
- TypeScript:
58
- ```ts
59
- import { autoHydrate } from '@coherent.js/client';
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
- document.addEventListener('DOMContentLoaded', () => {
62
- autoHydrate();
63
- });
64
- ```
105
+ ### Re-rendering
65
106
 
66
- ### Attaching custom handlers
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
- The client exposes an event registry you can populate during hydration.
114
+ ### State serialization and mismatches
69
115
 
70
116
  ```js
71
- import { registerEventHandler } from '@coherent.js/client';
117
+ import { serializeState, extractState, detectMismatch, reportMismatches } from '@coherent.js/client';
72
118
 
73
- registerEventHandler('increment', (el, evt, ctx) => {
74
- // custom logic using element, DOM event, and context
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
- TypeScript:
79
- ```ts
80
- import { registerEventHandler } from '@coherent.js/client';
124
+ ## Router
125
+
126
+ ```js
127
+ import { createRouter } from '@coherent.js/client/router';
81
128
 
82
- type Ctx = { state?: unknown };
129
+ const router = createRouter({ base: '/app' }); // or { mode: 'hash' }
83
130
 
84
- registerEventHandler('increment', (el: HTMLElement, evt: Event, ctx: Ctx) => {
85
- console.log('clicked', el, ctx);
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
- ## Notes on testing
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
- When testing client-side utilities in Node, provide light DOM shims (see repository tests under `packages/client/test/`). Example:
151
+ ## HMR
92
152
 
93
153
  ```js
94
- import { vi } from 'vitest';
154
+ import { hmrClient, createHotContext } from '@coherent.js/client';
155
+
156
+ hmrClient.initialize(); // connects to the dev server's WebSocket
95
157
 
96
- global.window = { __coherentEventRegistry: {}, addEventListener: vi.fn() };
97
- global.document = { querySelector: vi.fn(), querySelectorAll: vi.fn(() => []) };
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
- ## Development
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
- Run tests for this package:
103
- ```bash
104
- pnpm --filter @coherent.js/client run test
105
- ```
168
+ ## Notes on testing
106
169
 
107
- Watch mode:
108
- ```bash
109
- pnpm --filter @coherent.js/client run test:watch
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
- Type check:
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