bitwrench 2.0.22 → 2.0.24
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.txt +1 -1
- package/README.md +4 -3
- package/bin/bwmcp.js +3 -0
- package/dist/bitwrench-bccl.cjs.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +3 -3
- package/dist/bitwrench-lean.cjs.min.js +2 -2
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +3 -3
- package/dist/bitwrench-lean.es5.min.js +2 -2
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +3 -3
- package/dist/bitwrench-lean.esm.min.js +2 -2
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +3 -3
- package/dist/bitwrench-lean.umd.min.js +2 -2
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-css.cjs.js +1 -1
- package/dist/bitwrench-util-css.cjs.min.js +1 -1
- package/dist/bitwrench-util-css.es5.js +1 -1
- package/dist/bitwrench-util-css.es5.min.js +1 -1
- package/dist/bitwrench-util-css.esm.js +1 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -1
- package/dist/bitwrench-util-css.umd.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +3 -3
- package/dist/bitwrench.cjs.min.js +2 -2
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +1 -1
- package/dist/bitwrench.es5.js +3 -3
- package/dist/bitwrench.es5.min.js +2 -2
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +3 -3
- package/dist/bitwrench.esm.min.js +2 -2
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.umd.js +3 -3
- package/dist/bitwrench.umd.min.js +2 -2
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +65 -65
- package/dist/bwserve.cjs.js +2 -2
- package/dist/bwserve.esm.js +2 -2
- package/dist/sri.json +45 -45
- package/docs/README.md +76 -0
- package/docs/app-patterns.md +264 -0
- package/docs/bitwrench-mcp.md +426 -0
- package/docs/bitwrench_api.md +2232 -0
- package/docs/bw-attach.md +399 -0
- package/docs/bwserve.md +841 -0
- package/docs/cli.md +307 -0
- package/docs/component-cheatsheet.md +144 -0
- package/docs/component-library.md +1099 -0
- package/docs/framework-translation-table.md +33 -0
- package/docs/llm-bitwrench-guide.md +672 -0
- package/docs/routing.md +562 -0
- package/docs/state-management.md +767 -0
- package/docs/taco-format.md +373 -0
- package/docs/theming.md +309 -0
- package/docs/thinking-in-bitwrench.md +1457 -0
- package/docs/tutorial-bwserve.md +297 -0
- package/docs/tutorial-embedded.md +314 -0
- package/docs/tutorial-website.md +255 -0
- package/package.json +11 -3
- package/readme.html +5 -4
- package/src/mcp/knowledge.js +231 -0
- package/src/mcp/live.js +226 -0
- package/src/mcp/server.js +216 -0
- package/src/mcp/tools.js +369 -0
- package/src/mcp/transport.js +55 -0
- package/src/version.js +3 -3
package/docs/routing.md
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
# Client-Side Routing
|
|
2
|
+
|
|
3
|
+
## What Is Client-Side Routing?
|
|
4
|
+
|
|
5
|
+
In a traditional website, every page is a separate HTML file on the server. When you click a link, the browser sends a request, the server sends back a new page, and the entire screen refreshes. Every navigation is a full page load.
|
|
6
|
+
|
|
7
|
+
Client-side routing eliminates that round trip. A single HTML page loads once. When the user navigates, JavaScript intercepts the URL change, figures out which "view" to show, and swaps content in the DOM -- no server request, no page reload. The URL in the address bar still changes (so bookmarks and back/forward work), but the page never fully reloads.
|
|
8
|
+
|
|
9
|
+
This is what makes a Single-Page Application (SPA).
|
|
10
|
+
|
|
11
|
+
### How URLs Work in an SPA
|
|
12
|
+
|
|
13
|
+
The browser has two mechanisms that SPAs exploit:
|
|
14
|
+
|
|
15
|
+
**Hash fragment** -- The part of the URL after `#`. Changing the hash does not trigger a page reload. The browser fires a `hashchange` event that JavaScript can listen to. Example: `http://example.com/#/users/123`
|
|
16
|
+
|
|
17
|
+
**History API** -- `history.pushState()` changes the URL in the address bar without a page reload. The browser fires a `popstate` event when the user hits back/forward. Example: `http://example.com/users/123` (looks like a normal URL, but the page never reloaded)
|
|
18
|
+
|
|
19
|
+
Bitwrench supports both. Hash mode is the default because it works everywhere with zero server configuration.
|
|
20
|
+
|
|
21
|
+
### What Happens When You Navigate
|
|
22
|
+
|
|
23
|
+
Here is the full sequence when a user clicks a link or calls `bw.navigate()`:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
User clicks link
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
1. URL changes (hash or pushState)
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
2. Router reads the new URL
|
|
33
|
+
|
|
|
34
|
+
v
|
|
35
|
+
3. before() guard runs (can redirect or block)
|
|
36
|
+
|
|
|
37
|
+
v
|
|
38
|
+
4. URL is split into segments and matched against route patterns
|
|
39
|
+
|
|
|
40
|
+
v
|
|
41
|
+
5. Matched handler is called with extracted params
|
|
42
|
+
|
|
|
43
|
+
v
|
|
44
|
+
6. Handler returns a TACO object (the "page")
|
|
45
|
+
|
|
|
46
|
+
v
|
|
47
|
+
7. bw.DOM(target, taco) replaces the content area
|
|
48
|
+
|
|
|
49
|
+
v
|
|
50
|
+
8. bw.pub('bw:route', data) notifies subscribers
|
|
51
|
+
|
|
|
52
|
+
v
|
|
53
|
+
9. after() hook runs (analytics, scroll reset, etc.)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The key insight: "switching pages" is just calling `bw.DOM()` with different TACO content. There is no page object, no component lifecycle to manage. A "page" is a function that returns TACO.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
bw.router({
|
|
64
|
+
target: '#app',
|
|
65
|
+
routes: {
|
|
66
|
+
'/': function() { return { t: 'h1', c: 'Home' }; },
|
|
67
|
+
'/about': function() { return { t: 'h1', c: 'About' }; },
|
|
68
|
+
'/users/:id': function(params) {
|
|
69
|
+
return { t: 'div', c: 'User ' + params.id };
|
|
70
|
+
},
|
|
71
|
+
'*': function() { return { t: 'h1', c: '404 Not Found' }; }
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The router reads the current URL, matches a route, calls the handler, and renders the result into `#app`. It listens for URL changes (back/forward, hash changes) and re-renders automatically.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## How Route Matching Works
|
|
81
|
+
|
|
82
|
+
When the URL changes, the router must decide which handler to call. This happens in three steps:
|
|
83
|
+
|
|
84
|
+
### Step 1: Normalize the URL
|
|
85
|
+
|
|
86
|
+
The raw URL is cleaned up before matching:
|
|
87
|
+
- Query string is stripped and parsed separately (available as `params._query`)
|
|
88
|
+
- Double slashes are collapsed (`//users///` becomes `/users`)
|
|
89
|
+
- Trailing slashes are removed (`/users/` becomes `/users`)
|
|
90
|
+
- Empty paths become `/`
|
|
91
|
+
|
|
92
|
+
### Step 2: Split into segments
|
|
93
|
+
|
|
94
|
+
The normalized path is split on `/` into segments:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
/users/123/posts => ['', 'users', '123', 'posts']
|
|
98
|
+
/ => ['']
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Step 3: Match against route patterns
|
|
102
|
+
|
|
103
|
+
Routes are checked in this priority order:
|
|
104
|
+
|
|
105
|
+
**1. Exact match** -- Every segment matches literally.
|
|
106
|
+
```
|
|
107
|
+
Pattern: /users/new URL: /users/new => MATCH
|
|
108
|
+
Pattern: /users/new URL: /users/123 => no match
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**2. Parameterized match** -- Segments starting with `:` capture any value. Segment count must match exactly.
|
|
112
|
+
```
|
|
113
|
+
Pattern: /users/:id URL: /users/123 => MATCH, params.id = '123'
|
|
114
|
+
Pattern: /users/:id/posts URL: /users/123 => no match (segment count differs)
|
|
115
|
+
Pattern: /users/:id/posts/:pid URL: /users/42/posts/7 => MATCH, params = {id:'42', pid:'7'}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**3. Catch-all** -- Patterns ending with `/*` match any number of trailing segments. The captured portion goes into `params._rest`.
|
|
119
|
+
```
|
|
120
|
+
Pattern: /docs/* URL: /docs/api/colors => MATCH, params._rest = 'api/colors'
|
|
121
|
+
Pattern: /admin/:section/* URL: /admin/users/123/edit => MATCH, params = {section:'users', _rest:'123/edit'}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**4. Global wildcard** -- The pattern `*` (by itself) matches anything not matched above. This is your 404 handler.
|
|
125
|
+
```
|
|
126
|
+
Pattern: * URL: /anything => MATCH
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Within the same priority level, routes are checked in the order you registered them. **Register specific routes before general ones:**
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
routes: {
|
|
133
|
+
'/users/new': newUserPage, // checked first (exact)
|
|
134
|
+
'/users/:id': userDetailPage, // checked second (parameterized)
|
|
135
|
+
'*': notFoundPage // checked last (wildcard)
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Query Strings
|
|
140
|
+
|
|
141
|
+
Query strings are parsed but do not affect which route matches:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// URL: /users/123?tab=posts&page=2
|
|
145
|
+
'/users/:id': function(params) {
|
|
146
|
+
params.id; // '123'
|
|
147
|
+
params._query.tab; // 'posts'
|
|
148
|
+
params._query.page; // '2'
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Full Pattern Reference
|
|
153
|
+
|
|
154
|
+
| Pattern | Example URL | Params |
|
|
155
|
+
|---------|-------------|--------|
|
|
156
|
+
| `/` | `/` | `{}` |
|
|
157
|
+
| `/users` | `/users` | `{}` |
|
|
158
|
+
| `/users/:id` | `/users/123` | `{ id: '123' }` |
|
|
159
|
+
| `/users/:id/posts/:pid` | `/users/42/posts/7` | `{ id: '42', pid: '7' }` |
|
|
160
|
+
| `/docs/*` | `/docs/api/colors` | `{ _rest: 'api/colors' }` |
|
|
161
|
+
| `/admin/:section/*` | `/admin/users/123/edit` | `{ section: 'users', _rest: '123/edit' }` |
|
|
162
|
+
| `*` | `/anything` | `{}` |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Hash Mode vs History Mode
|
|
167
|
+
|
|
168
|
+
### Hash mode (default)
|
|
169
|
+
|
|
170
|
+
URLs look like `http://example.com/#/users/123`.
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
bw.router({
|
|
174
|
+
mode: 'hash', // default, can be omitted
|
|
175
|
+
target: '#app',
|
|
176
|
+
routes: { ... }
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**How it works:** The router sets `window.location.hash` and listens for `hashchange` events. The hash is never sent to the server, so no server configuration is needed.
|
|
181
|
+
|
|
182
|
+
**Pros:** Works everywhere (including old browsers). No server config. Files can be opened from disk (`file://`).
|
|
183
|
+
|
|
184
|
+
**Cons:** URLs have a `#` in them. Some people find this ugly.
|
|
185
|
+
|
|
186
|
+
### History mode
|
|
187
|
+
|
|
188
|
+
URLs look like `http://example.com/users/123` -- clean, no `#`.
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
bw.router({
|
|
192
|
+
mode: 'history',
|
|
193
|
+
base: '/app', // optional: strip this prefix before matching
|
|
194
|
+
target: '#app',
|
|
195
|
+
routes: { ... }
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**How it works:** The router calls `history.pushState()` to change the URL and listens for `popstate` events (back/forward buttons).
|
|
200
|
+
|
|
201
|
+
**Pros:** Clean URLs. Looks like a traditional website.
|
|
202
|
+
|
|
203
|
+
**Cons:** Requires server configuration. The server must return your `index.html` for all routes, because if a user bookmarks `/users/123` and visits it directly, the server needs to serve the SPA shell (not a 404). This is called "SPA fallback" or "history fallback."
|
|
204
|
+
|
|
205
|
+
With `base: '/app'`, a URL like `http://example.com/app/users/123` is matched as `/users/123`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Route Handlers
|
|
210
|
+
|
|
211
|
+
Handlers are plain functions. They receive a `params` object and return a TACO (or null).
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
'/users/:id': function(params) {
|
|
215
|
+
return bw.makeCard({
|
|
216
|
+
title: 'User ' + params.id,
|
|
217
|
+
content: 'Tab: ' + (params._query.tab || 'profile')
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
If a handler returns `null` or `undefined`, the target element is not updated. This is useful for routes that only need side effects (analytics, logging).
|
|
223
|
+
|
|
224
|
+
If no `target` is configured, handlers still run and pub/sub events still fire -- useful for pub/sub-only routing where you manage rendering yourself.
|
|
225
|
+
|
|
226
|
+
### Stateful route handlers
|
|
227
|
+
|
|
228
|
+
Route handlers can return stateful TACOs with `o.state` and `o.render`:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
function dashboard() {
|
|
232
|
+
return {
|
|
233
|
+
t: 'div',
|
|
234
|
+
o: {
|
|
235
|
+
state: { data: null },
|
|
236
|
+
mounted: function(el) {
|
|
237
|
+
fetch('/api/stats').then(function(r) { return r.json(); })
|
|
238
|
+
.then(function(d) { el._bw_state.data = d; bw.update(el); });
|
|
239
|
+
},
|
|
240
|
+
render: function(el) {
|
|
241
|
+
var s = el._bw_state;
|
|
242
|
+
bw.DOM(el, s.data
|
|
243
|
+
? bw.makeTable({ data: s.data, sortable: true })
|
|
244
|
+
: { t: 'p', c: 'Loading...' }
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
When the router mounts this TACO, `mounted` fires, fetches data, updates state, and triggers a re-render. The route handler is just a function that returns any valid TACO -- the full component model (Level 0 through Level 2) is available.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Navigation
|
|
257
|
+
|
|
258
|
+
### bw.navigate(path, opts)
|
|
259
|
+
|
|
260
|
+
Programmatic navigation. Delegates to the active router.
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
bw.navigate('/users/123');
|
|
264
|
+
bw.navigate('/users/123', { replace: true }); // replace history entry (no back)
|
|
265
|
+
bw.navigate('/search?q=hello'); // query strings preserved
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### bw.link(path, content, attrs)
|
|
269
|
+
|
|
270
|
+
Returns a TACO `<a>` element with navigation wired up:
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
bw.link('/about', 'About Us', { class: 'nav-item' })
|
|
274
|
+
// Returns:
|
|
275
|
+
// { t: 'a', a: { href: '#/about', class: 'nav-item', onclick: ... }, c: 'About Us' }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The `onclick` handler calls `e.preventDefault()` and `bw.navigate(path)`. The `href` is set to `#` + path (hash mode) so right-click "copy link" and middle-click "open in new tab" still work.
|
|
279
|
+
|
|
280
|
+
Use `bw.link()` instead of raw `<a>` tags for navigation within the SPA. External links (to other sites) should use normal TACO anchors: `{ t: 'a', a: { href: 'https://...' }, c: 'External' }`.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Guards and Hooks
|
|
285
|
+
|
|
286
|
+
### before(toPath, fromPath)
|
|
287
|
+
|
|
288
|
+
Called before each navigation. Use it for authentication checks, redirects, or blocking.
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
bw.router({
|
|
292
|
+
target: '#app',
|
|
293
|
+
routes: { ... },
|
|
294
|
+
before: function(to, from) {
|
|
295
|
+
// Redirect: return a path string
|
|
296
|
+
if (to === '/admin' && !isLoggedIn) return '/login';
|
|
297
|
+
|
|
298
|
+
// Block: return false
|
|
299
|
+
if (to === '/locked') return false;
|
|
300
|
+
|
|
301
|
+
// Allow: return anything else (undefined, null, true)
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
| Return value | Effect |
|
|
307
|
+
|-------------|--------|
|
|
308
|
+
| `string` | Redirect to that path |
|
|
309
|
+
| `false` | Block navigation (URL and view unchanged) |
|
|
310
|
+
| anything else | Allow navigation |
|
|
311
|
+
|
|
312
|
+
### after(toPath, fromPath)
|
|
313
|
+
|
|
314
|
+
Called after each navigation completes. Use it for analytics, logging, scroll reset.
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
after: function(to, from) {
|
|
318
|
+
window.scrollTo(0, 0);
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Pub/Sub Integration
|
|
325
|
+
|
|
326
|
+
Every route change publishes a `bw:route` event:
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
bw.sub('bw:route', function(data) {
|
|
330
|
+
// data.path -- current path (e.g., '/users/123')
|
|
331
|
+
// data.params -- matched params (e.g., { id: '123', _query: {} })
|
|
332
|
+
// data.query -- parsed query string object
|
|
333
|
+
// data.from -- previous path
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
This lets any component react to route changes without being coupled to the router. Common uses:
|
|
338
|
+
|
|
339
|
+
**Highlight active nav item:**
|
|
340
|
+
```javascript
|
|
341
|
+
bw.sub('bw:route', function(data) {
|
|
342
|
+
navEl.bw.setActive(data.path);
|
|
343
|
+
}, navEl); // auto-unsubscribes when navEl is removed
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Update page title:**
|
|
347
|
+
```javascript
|
|
348
|
+
var titles = { '/': 'Home', '/about': 'About', '/contact': 'Contact' };
|
|
349
|
+
bw.sub('bw:route', function(data) {
|
|
350
|
+
document.title = titles[data.path] || 'My App';
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Log analytics:**
|
|
355
|
+
```javascript
|
|
356
|
+
bw.sub('bw:route', function(data) {
|
|
357
|
+
analytics.pageView(data.path);
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Patterns
|
|
364
|
+
|
|
365
|
+
### SPA with persistent nav and footer
|
|
366
|
+
|
|
367
|
+
The router only controls the content area. Nav and footer are mounted separately and persist across route changes:
|
|
368
|
+
|
|
369
|
+
```html
|
|
370
|
+
<div id="nav-root"></div>
|
|
371
|
+
<div id="app"></div>
|
|
372
|
+
<div id="footer-root"></div>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
376
|
+
// Mount persistent UI
|
|
377
|
+
bw.DOM('#nav-root', makeNavBar());
|
|
378
|
+
bw.DOM('#footer-root', makeFooter());
|
|
379
|
+
|
|
380
|
+
// Router only swaps #app content
|
|
381
|
+
bw.router({
|
|
382
|
+
target: '#app',
|
|
383
|
+
routes: {
|
|
384
|
+
'/': homePage,
|
|
385
|
+
'/about': aboutPage,
|
|
386
|
+
'/contact': contactPage,
|
|
387
|
+
'*': notFoundPage
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Nav bar that highlights the active route
|
|
393
|
+
|
|
394
|
+
```javascript
|
|
395
|
+
function makeNav() {
|
|
396
|
+
var links = [
|
|
397
|
+
{ path: '/', label: 'Home' },
|
|
398
|
+
{ path: '/about', label: 'About' },
|
|
399
|
+
{ path: '/contact', label: 'Contact' }
|
|
400
|
+
];
|
|
401
|
+
return {
|
|
402
|
+
t: 'nav',
|
|
403
|
+
o: {
|
|
404
|
+
state: { active: '/' },
|
|
405
|
+
mounted: function(el) {
|
|
406
|
+
bw.sub('bw:route', function(d) {
|
|
407
|
+
el._bw_state.active = d.path;
|
|
408
|
+
bw.update(el);
|
|
409
|
+
}, el);
|
|
410
|
+
},
|
|
411
|
+
render: function(el) {
|
|
412
|
+
var s = el._bw_state;
|
|
413
|
+
bw.DOM(el, {
|
|
414
|
+
t: 'ul', c: links.map(function(link) {
|
|
415
|
+
return { t: 'li', a: {
|
|
416
|
+
style: link.path === s.active ? 'font-weight:bold' : ''
|
|
417
|
+
}, c: bw.link(link.path, link.label) };
|
|
418
|
+
})
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Auth guard with login redirect
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
var isLoggedIn = false;
|
|
430
|
+
|
|
431
|
+
bw.router({
|
|
432
|
+
target: '#app',
|
|
433
|
+
routes: {
|
|
434
|
+
'/': homePage,
|
|
435
|
+
'/login': loginPage,
|
|
436
|
+
'/dashboard': dashboardPage,
|
|
437
|
+
'*': notFoundPage
|
|
438
|
+
},
|
|
439
|
+
before: function(to) {
|
|
440
|
+
if (to === '/dashboard' && !isLoggedIn) return '/login';
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Router with shared state store
|
|
446
|
+
|
|
447
|
+
Combine the router with a store pattern for multi-view apps. The store holds data; views subscribe to the slices they need:
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
var store = { users: [], stats: {} };
|
|
451
|
+
|
|
452
|
+
function updateStore(key, value) {
|
|
453
|
+
store[key] = value;
|
|
454
|
+
bw.pub('store:' + key, value);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function usersPage() {
|
|
458
|
+
return {
|
|
459
|
+
t: 'div',
|
|
460
|
+
o: {
|
|
461
|
+
state: {},
|
|
462
|
+
mounted: function(el) {
|
|
463
|
+
bw.sub('store:users', function() { bw.update(el); }, el);
|
|
464
|
+
},
|
|
465
|
+
render: function(el) {
|
|
466
|
+
bw.DOM(el, bw.makeTable({
|
|
467
|
+
data: store.users,
|
|
468
|
+
columns: ['name', 'role', 'status'],
|
|
469
|
+
sortable: true
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
bw.router({
|
|
477
|
+
target: '#app',
|
|
478
|
+
routes: {
|
|
479
|
+
'/': overviewPage,
|
|
480
|
+
'/users': usersPage,
|
|
481
|
+
'*': notFoundPage
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
See [State Management: Shared State Across Views](state-management.md#shared-state-across-views) for details on the store pattern.
|
|
487
|
+
|
|
488
|
+
### Complementing bwserve
|
|
489
|
+
|
|
490
|
+
The client router complements bwserve's server-side `app.page()`. Use bwserve for top-level page delivery and the client router for sub-navigation within a page:
|
|
491
|
+
|
|
492
|
+
```javascript
|
|
493
|
+
// Server handles top-level pages
|
|
494
|
+
app.page('/dashboard', function(client) {
|
|
495
|
+
client.render('#app', dashboardShell());
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Client handles tab navigation within the dashboard
|
|
499
|
+
bw.router({
|
|
500
|
+
target: '#dashboard-content',
|
|
501
|
+
mode: 'hash',
|
|
502
|
+
routes: {
|
|
503
|
+
'/overview': overviewTab,
|
|
504
|
+
'/analytics': analyticsTab,
|
|
505
|
+
'/settings': settingsTab
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Cleanup
|
|
513
|
+
|
|
514
|
+
Call `r.destroy()` to remove event listeners and stop the router:
|
|
515
|
+
|
|
516
|
+
```javascript
|
|
517
|
+
var r = bw.router({ ... });
|
|
518
|
+
|
|
519
|
+
// Later, when done:
|
|
520
|
+
r.destroy();
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
After `destroy()`, `bw.navigate()` calls are no-ops and no more `bw:route` events are published.
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## API Summary
|
|
528
|
+
|
|
529
|
+
| Function | Description |
|
|
530
|
+
|----------|-------------|
|
|
531
|
+
| `bw.router(config)` | Create and start a router. Returns `{ navigate, current, destroy }` |
|
|
532
|
+
| `bw.navigate(path, opts)` | Programmatic navigation (delegates to active router) |
|
|
533
|
+
| `bw.link(path, content, attrs)` | Returns TACO `<a>` with navigation wired |
|
|
534
|
+
| `bw:route` (pub/sub topic) | Published on every route change with `{ path, params, query, from }` |
|
|
535
|
+
|
|
536
|
+
### bw.router(config) options
|
|
537
|
+
|
|
538
|
+
| Option | Type | Default | Description |
|
|
539
|
+
|--------|------|---------|-------------|
|
|
540
|
+
| `routes` | `Object` | (required) | Map of route patterns to handler functions |
|
|
541
|
+
| `target` | `string` | `null` | CSS selector where handler output is mounted via `bw.DOM()` |
|
|
542
|
+
| `mode` | `string` | `'hash'` | `'hash'` or `'history'` |
|
|
543
|
+
| `base` | `string` | `'/'` | Base path to strip in history mode |
|
|
544
|
+
| `before` | `function` | `null` | Guard called before each navigation |
|
|
545
|
+
| `after` | `function` | `null` | Hook called after each navigation |
|
|
546
|
+
|
|
547
|
+
### Router object methods
|
|
548
|
+
|
|
549
|
+
| Method | Description |
|
|
550
|
+
|--------|-------------|
|
|
551
|
+
| `r.navigate(path, opts)` | Navigate to a path (same as `bw.navigate()`) |
|
|
552
|
+
| `r.current()` | Returns `{ path, params, query }` for current route |
|
|
553
|
+
| `r.destroy()` | Remove listeners, stop routing |
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## Related
|
|
558
|
+
|
|
559
|
+
- [App Patterns](app-patterns.md) -- Multi-Page SPA pattern with router + shared state
|
|
560
|
+
- [State Management](state-management.md) -- Three-level component model, store pattern
|
|
561
|
+
- [Component Cheat Sheet](component-cheatsheet.md) -- All 50+ components at a glance
|
|
562
|
+
- [examples/dashboard-spa/](../examples/dashboard-spa/) -- Working SPA with 4 routed views
|