aberdeen 1.5.0 → 1.7.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 +5 -11
- package/dist/aberdeen.d.ts +234 -128
- package/dist/aberdeen.js +212 -103
- package/dist/aberdeen.js.map +3 -3
- package/dist/dispatcher.d.ts +10 -7
- package/dist/dispatcher.js +11 -10
- package/dist/dispatcher.js.map +3 -3
- package/dist/route.d.ts +17 -0
- package/dist/route.js +65 -23
- package/dist/route.js.map +3 -3
- package/dist-min/aberdeen.js +9 -7
- package/dist-min/aberdeen.js.map +3 -3
- package/dist-min/dispatcher.js +2 -2
- package/dist-min/dispatcher.js.map +3 -3
- package/dist-min/route.js +2 -2
- package/dist-min/route.js.map +3 -3
- package/html-to-aberdeen +3 -6
- package/package.json +5 -2
- package/skill/SKILL.md +791 -206
- package/skill/aberdeen.md +2338 -0
- package/skill/dispatcher.md +129 -0
- package/skill/prediction.md +73 -0
- package/skill/route.md +277 -0
- package/skill/transitions.md +59 -0
- package/src/aberdeen.ts +490 -244
- package/src/dispatcher.ts +16 -13
- package/src/route.ts +93 -22
- package/skill/references/prediction.md +0 -45
- package/skill/references/routing.md +0 -81
- package/skill/references/transitions.md +0 -52
package/src/dispatcher.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Symbol to return when a custom {@link Dispatcher.addRoute} matcher cannot match a segment.
|
|
3
3
|
*/
|
|
4
|
-
export const
|
|
4
|
+
export const MATCH_FAILED: unique symbol = Symbol("MATCH_FAILED");
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Special {@link Dispatcher.addRoute} matcher that matches the rest of the segments as an array of strings.
|
|
8
8
|
*/
|
|
9
|
-
export const
|
|
9
|
+
export const MATCH_REST: unique symbol = Symbol("MATCH_REST");
|
|
10
10
|
|
|
11
|
-
type Matcher = string | ((segment: string) => any) | typeof
|
|
11
|
+
type Matcher = string | ((segment: string) => any) | typeof MATCH_REST;
|
|
12
12
|
|
|
13
13
|
type ExtractParamType<M> = M extends string
|
|
14
14
|
? never : (
|
|
15
15
|
M extends ((segment: string) => infer R)
|
|
16
|
-
? Exclude<R, typeof
|
|
17
|
-
: (M extends typeof
|
|
16
|
+
? Exclude<R, typeof MATCH_FAILED>
|
|
17
|
+
: (M extends typeof MATCH_REST ? string[] : never)
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
type ParamsFromMatchers<T extends Matcher[]> = T extends [infer M1, ...infer Rest]
|
|
@@ -38,6 +38,9 @@ interface DispatcherRoute {
|
|
|
38
38
|
* Example usage:
|
|
39
39
|
*
|
|
40
40
|
* ```ts
|
|
41
|
+
* import * as route from 'aberdeen/route';
|
|
42
|
+
* import { Dispatcher, MATCH_REST } from 'aberdeen/dispatcher';
|
|
43
|
+
*
|
|
41
44
|
* const dispatcher = new Dispatcher();
|
|
42
45
|
*
|
|
43
46
|
* dispatcher.addRoute("user", Number, "stream", String, (id, stream) => {
|
|
@@ -47,7 +50,7 @@ interface DispatcherRoute {
|
|
|
47
50
|
* dispatcher.dispatch(["user", "42", "stream", "music"]);
|
|
48
51
|
* // Logs: User 42, stream music
|
|
49
52
|
*
|
|
50
|
-
* dispatcher.addRoute("search",
|
|
53
|
+
* dispatcher.addRoute("search", MATCH_REST, (terms: string[]) => {
|
|
51
54
|
* console.log("Search terms:", terms);
|
|
52
55
|
* });
|
|
53
56
|
*
|
|
@@ -62,8 +65,8 @@ export class Dispatcher {
|
|
|
62
65
|
* Add a route with matchers and a handler function.
|
|
63
66
|
* @param args An array of matchers followed by a handler function. Each matcher can be:
|
|
64
67
|
* - A string: matches exactly that string.
|
|
65
|
-
* - A function: takes a string segment and returns a value (of any type) if it matches, or {@link
|
|
66
|
-
* - The special {@link
|
|
68
|
+
* - A function: takes a string segment and returns a value (of any type) if it matches, or {@link MATCH_FAILED} if it doesn't match. The return value (if not `MATCH_FAILED` and not `NaN`) is passed as a parameter to the handler function. The standard JavaScript functions `Number` and `String` can be used to match numeric and string segments respectively.
|
|
69
|
+
* - The special {@link MATCH_REST} symbol: matches the rest of the segments as an array of strings. Only one `MATCH_REST` is allowed.
|
|
67
70
|
* @template T - Array of matcher types.
|
|
68
71
|
* @template H - Handler function type, inferred from the matchers.
|
|
69
72
|
*/
|
|
@@ -75,9 +78,9 @@ export class Dispatcher {
|
|
|
75
78
|
throw new Error("Last argument should be a handler function");
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
const restCount = matchers.filter(m => m ===
|
|
81
|
+
const restCount = matchers.filter(m => m === MATCH_REST).length;
|
|
79
82
|
if (restCount > 1) {
|
|
80
|
-
throw new Error("Only one
|
|
83
|
+
throw new Error("Only one MATCH_REST is allowed");
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
this.routes.push({ matchers, handler });
|
|
@@ -105,7 +108,7 @@ function matchRoute(route: DispatcherRoute, segments: string[]): any[] | undefin
|
|
|
105
108
|
let segmentIndex = 0;
|
|
106
109
|
|
|
107
110
|
for (const matcher of route.matchers) {
|
|
108
|
-
if (matcher ===
|
|
111
|
+
if (matcher === MATCH_REST) {
|
|
109
112
|
const len = segments.length - (route.matchers.length - 1);
|
|
110
113
|
if (len < 0) return;
|
|
111
114
|
args.push(segments.slice(segmentIndex, segmentIndex + len));
|
|
@@ -120,11 +123,11 @@ function matchRoute(route: DispatcherRoute, segments: string[]): any[] | undefin
|
|
|
120
123
|
if (segment !== matcher) return;
|
|
121
124
|
} else if (typeof matcher === "function") {
|
|
122
125
|
const result = matcher(segment);
|
|
123
|
-
if (result ===
|
|
126
|
+
if (result === MATCH_FAILED || (typeof result === 'number' && isNaN(result))) return;
|
|
124
127
|
args.push(result);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
segmentIndex++;
|
|
128
131
|
}
|
|
129
|
-
return args; // success!
|
|
132
|
+
if (segmentIndex === segments.length) return args; // success!
|
|
130
133
|
}
|
package/src/route.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {clean,
|
|
1
|
+
import {clean, $, proxy, runQueue, unproxy, copy, merge, clone, leakScope} from "./aberdeen.js";
|
|
2
2
|
|
|
3
3
|
type NavType = "load" | "back" | "forward" | "go" | "push";
|
|
4
4
|
|
|
@@ -44,13 +44,18 @@ export function setLog(value: boolean | ((...args: any[]) => void)) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
declare const ABERDEEN_FAKE_WINDOW: Window | undefined;
|
|
48
|
+
const windowE = typeof ABERDEEN_FAKE_WINDOW !== 'undefined'? ABERDEEN_FAKE_WINDOW : window;
|
|
49
|
+
const locationE = windowE.location;
|
|
50
|
+
const historyE = windowE.history;
|
|
51
|
+
|
|
47
52
|
function getRouteFromBrowser(): Route {
|
|
48
53
|
return toCanonRoute({
|
|
49
|
-
path:
|
|
50
|
-
hash:
|
|
51
|
-
search: Object.fromEntries(new URLSearchParams(
|
|
52
|
-
state:
|
|
53
|
-
}, "load", (
|
|
54
|
+
path: locationE.pathname,
|
|
55
|
+
hash: locationE.hash,
|
|
56
|
+
search: Object.fromEntries(new URLSearchParams(locationE.search)),
|
|
57
|
+
state: historyE.state?.state || {},
|
|
58
|
+
}, "load", (historyE.state?.stack?.length || 0) + 1);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/**
|
|
@@ -149,7 +154,7 @@ function targetToPartial(target: RouteTarget) {
|
|
|
149
154
|
* ```
|
|
150
155
|
*/
|
|
151
156
|
export function go(target: RouteTarget, nav: NavType = "go"): void {
|
|
152
|
-
const stack: string[] =
|
|
157
|
+
const stack: string[] = historyE.state?.stack || [];
|
|
153
158
|
|
|
154
159
|
prevStack = stack.concat(JSON.stringify(unproxy(current)));
|
|
155
160
|
|
|
@@ -157,7 +162,7 @@ export function go(target: RouteTarget, nav: NavType = "go"): void {
|
|
|
157
162
|
copy(current, newRoute);
|
|
158
163
|
|
|
159
164
|
log(nav, newRoute);
|
|
160
|
-
|
|
165
|
+
historyE.pushState({state: newRoute.state, stack: prevStack}, "", getUrl(newRoute));
|
|
161
166
|
|
|
162
167
|
runQueue();
|
|
163
168
|
}
|
|
@@ -170,9 +175,9 @@ export function go(target: RouteTarget, nav: NavType = "go"): void {
|
|
|
170
175
|
* @param target Same as for {@link go}, but merged into the current route instead deleting all state.
|
|
171
176
|
*/
|
|
172
177
|
export function push(target: RouteTarget): void {
|
|
173
|
-
|
|
174
|
-
merge(
|
|
175
|
-
go(
|
|
178
|
+
const c = clone(unproxy(current));
|
|
179
|
+
merge(c, targetToPartial(target));
|
|
180
|
+
go(c);
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
/**
|
|
@@ -184,13 +189,13 @@ export function push(target: RouteTarget): void {
|
|
|
184
189
|
*/
|
|
185
190
|
export function back(target: RouteTarget = {}): void {
|
|
186
191
|
const partial = targetToPartial(target);
|
|
187
|
-
const stack: string[] =
|
|
192
|
+
const stack: string[] = historyE.state?.stack || [];
|
|
188
193
|
for(let i = stack.length - 1; i >= 0; i--) {
|
|
189
194
|
const histRoute: Route = JSON.parse(stack[i]);
|
|
190
195
|
if (equal(histRoute, partial, true)) {
|
|
191
196
|
const pages = i - stack.length;
|
|
192
197
|
log(`back`, pages, histRoute);
|
|
193
|
-
|
|
198
|
+
historyE.go(pages);
|
|
194
199
|
return;
|
|
195
200
|
}
|
|
196
201
|
}
|
|
@@ -209,13 +214,13 @@ export function back(target: RouteTarget = {}): void {
|
|
|
209
214
|
*/
|
|
210
215
|
export function up(stripCount: number = 1): void {
|
|
211
216
|
const currentP = unproxy(current).p;
|
|
212
|
-
const stack: string[] =
|
|
217
|
+
const stack: string[] = historyE.state?.stack || [];
|
|
213
218
|
for(let i = stack.length - 1; i >= 0; i--) {
|
|
214
219
|
const histRoute: Route = JSON.parse(stack[i]);
|
|
215
220
|
if (histRoute.p.length < currentP.length && equal(histRoute.p, currentP.slice(0, histRoute.p.length), false)) {
|
|
216
221
|
// This route is shorter and matches the start of the current path
|
|
217
222
|
log(`up to ${i+1} / ${stack.length}`, histRoute);
|
|
218
|
-
|
|
223
|
+
historyE.go(i - stack.length);
|
|
219
224
|
return;
|
|
220
225
|
}
|
|
221
226
|
}
|
|
@@ -235,7 +240,7 @@ export function up(stripCount: number = 1): void {
|
|
|
235
240
|
* The scroll position will be persisted in `route.aux.scroll.<name>`.
|
|
236
241
|
*/
|
|
237
242
|
export function persistScroll(name = "main") {
|
|
238
|
-
const el =
|
|
243
|
+
const el = $()!;
|
|
239
244
|
el.addEventListener("scroll", onScroll);
|
|
240
245
|
clean(() => el.removeEventListener("scroll", onScroll));
|
|
241
246
|
|
|
@@ -253,6 +258,72 @@ export function persistScroll(name = "main") {
|
|
|
253
258
|
}
|
|
254
259
|
}
|
|
255
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Intercept clicks and Enter key presses on links (`<a>` tags) and use Aberdeen routing
|
|
263
|
+
* instead of browser navigation for local paths (paths without a protocol or host).
|
|
264
|
+
*
|
|
265
|
+
* This allows you to use regular HTML anchor tags for navigation without needing to
|
|
266
|
+
* manually attach click handlers to each link.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```js
|
|
270
|
+
* // In your root component:
|
|
271
|
+
* route.interceptLinks();
|
|
272
|
+
*
|
|
273
|
+
* // Now you can use regular anchor tags:
|
|
274
|
+
* $('a text=About href=/corporate/about');
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export function interceptLinks() {
|
|
278
|
+
$({
|
|
279
|
+
click: handleEvent,
|
|
280
|
+
keydown: handleKeyEvent,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
function handleKeyEvent(e: KeyboardEvent) {
|
|
284
|
+
if (e.key === "Enter") {
|
|
285
|
+
handleEvent(e);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function handleEvent(e: Event) {
|
|
290
|
+
// Find the closest <a> tag
|
|
291
|
+
let target = e.target as HTMLElement | null;
|
|
292
|
+
while (target && target.tagName?.toUpperCase() !== "A") {
|
|
293
|
+
target = target.parentElement;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!target) return;
|
|
297
|
+
|
|
298
|
+
const anchor = target as HTMLAnchorElement;
|
|
299
|
+
const href = anchor.getAttribute("href");
|
|
300
|
+
|
|
301
|
+
if (!href) return;
|
|
302
|
+
|
|
303
|
+
// Skip hash-only links
|
|
304
|
+
if (href.startsWith("#")) return;
|
|
305
|
+
|
|
306
|
+
// Skip if it has a protocol or is protocol-relative (// or contains : before any / ? #)
|
|
307
|
+
if (href.startsWith("//") || /^[^/?#]+:/.test(href)) return;
|
|
308
|
+
|
|
309
|
+
// Skip if the link has target or download attribute
|
|
310
|
+
if (anchor.getAttribute("target") || anchor.getAttribute("download")) return;
|
|
311
|
+
|
|
312
|
+
// Skip if modifier keys are pressed (Ctrl/Cmd click to open in new tab)
|
|
313
|
+
if (typeof MouseEvent !== 'undefined' && e instanceof MouseEvent && (e.ctrlKey || e.metaKey || e.shiftKey)) return;
|
|
314
|
+
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
|
|
317
|
+
// Parse using URL to handle both absolute and relative paths correctly
|
|
318
|
+
const url = new URL(href, locationE.href);
|
|
319
|
+
go({
|
|
320
|
+
path: url.pathname,
|
|
321
|
+
search: Object.fromEntries(url.searchParams),
|
|
322
|
+
hash: url.hash,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
256
327
|
let prevStack: string[];
|
|
257
328
|
|
|
258
329
|
/**
|
|
@@ -265,7 +336,7 @@ export const current: Route = proxy({}) as Route;
|
|
|
265
336
|
* @internal
|
|
266
337
|
* */
|
|
267
338
|
export function reset() {
|
|
268
|
-
prevStack =
|
|
339
|
+
prevStack = historyE.state?.stack || [];
|
|
269
340
|
const initRoute = getRouteFromBrowser();
|
|
270
341
|
log('initial', initRoute);
|
|
271
342
|
copy(unproxy(current), initRoute);
|
|
@@ -273,12 +344,12 @@ export function reset() {
|
|
|
273
344
|
reset();
|
|
274
345
|
|
|
275
346
|
// Handle browser history back and forward
|
|
276
|
-
|
|
347
|
+
windowE.addEventListener("popstate", function(event: PopStateEvent) {
|
|
277
348
|
const newRoute = getRouteFromBrowser();
|
|
278
349
|
|
|
279
350
|
// If the stack length changes, and at least the top-most shared entry is the same,
|
|
280
351
|
// we'll interpret this as a "back" or "forward" navigation.
|
|
281
|
-
const stack: string[] =
|
|
352
|
+
const stack: string[] = historyE.state?.stack || [];
|
|
282
353
|
if (stack.length !== prevStack.length) {
|
|
283
354
|
const maxIndex = Math.min(prevStack.length, stack.length) - 1;
|
|
284
355
|
if (maxIndex < 0 || stack[maxIndex] === prevStack[maxIndex]) {
|
|
@@ -306,16 +377,16 @@ leakScope(() => {
|
|
|
306
377
|
$(() => {
|
|
307
378
|
|
|
308
379
|
// First normalize `route`
|
|
309
|
-
const stack =
|
|
380
|
+
const stack = historyE.state?.stack || [];
|
|
310
381
|
const newRoute = toCanonRoute(current, unproxy(current).nav, stack.length + 1);
|
|
311
382
|
copy(current, newRoute);
|
|
312
383
|
|
|
313
384
|
// Then replace the current browser state if something actually changed
|
|
314
385
|
const state = {state: newRoute.state, stack};
|
|
315
386
|
const url = getUrl(newRoute);
|
|
316
|
-
if (url !==
|
|
387
|
+
if (url !== locationE.pathname + locationE.search + locationE.hash || !equal(historyE.state, state, false)) {
|
|
317
388
|
log('replaceState', newRoute, state, url);
|
|
318
|
-
|
|
389
|
+
historyE.replaceState(state, "", url);
|
|
319
390
|
}
|
|
320
391
|
});
|
|
321
392
|
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Prediction (Optimistic UI)
|
|
2
|
-
|
|
3
|
-
Apply UI changes immediately, auto-revert when server responds.
|
|
4
|
-
|
|
5
|
-
## API
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { applyPrediction, applyCanon } from 'aberdeen/prediction';
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
### `applyPrediction(func)`
|
|
12
|
-
Runs function and records all proxy changes as a "prediction".
|
|
13
|
-
Returns a `Patch` to use as `dropPatches` later, when the server responds.
|
|
14
|
-
|
|
15
|
-
### `applyCanon(func?, dropPatches?)`
|
|
16
|
-
1. Reverts all predictions
|
|
17
|
-
2. Runs `func` (typically applies server data)
|
|
18
|
-
3. Drops specified patches
|
|
19
|
-
4. Re-applies remaining predictions that still apply cleanly
|
|
20
|
-
|
|
21
|
-
## Example
|
|
22
|
-
```typescript
|
|
23
|
-
async function toggleTodo(todo: Todo) {
|
|
24
|
-
// Optimistic update
|
|
25
|
-
const patch = applyPrediction(() => {
|
|
26
|
-
todo.done = !todo.done;
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const data = await api.updateTodo(todo.id, { done: todo.done });
|
|
31
|
-
|
|
32
|
-
// Server responded - apply canonical state
|
|
33
|
-
applyCanon(() => {
|
|
34
|
-
Object.assign(todo, data);
|
|
35
|
-
}, [patch]);
|
|
36
|
-
} catch {
|
|
37
|
-
// On error, just drop the prediction to revert
|
|
38
|
-
applyCanon(undefined, [patch]);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## When to Use
|
|
44
|
-
- When you want immediate UI feedback for user actions for which a server is authoritative.
|
|
45
|
-
- As doing this manually for each such case is tedious, this should usually be integrated into the data updating/fetching layer.
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Routing and Dispatching
|
|
2
|
-
|
|
3
|
-
## Router (`aberdeen/route`)
|
|
4
|
-
|
|
5
|
-
The `current` object is a reactive proxy of the current URL state.
|
|
6
|
-
|
|
7
|
-
### Properties
|
|
8
|
-
| Property | Type | Description |
|
|
9
|
-
|----------|------|-------------|
|
|
10
|
-
| `path` | `string` | Normalized path (e.g., `/users/123`) |
|
|
11
|
-
| `p` | `string[]` | Path segments (e.g., `['users', '123']`) |
|
|
12
|
-
| `search` | `Record<string,string>` | Query parameters |
|
|
13
|
-
| `hash` | `string` | URL hash including `#` |
|
|
14
|
-
| `state` | `Record<string,any>` | JSON-compatible state data |
|
|
15
|
-
| `nav` | `NavType` | How we got here: `load`, `back`, `forward`, `go`, `push` |
|
|
16
|
-
| `depth` | `number` | Navigation stack depth (starts at 1) |
|
|
17
|
-
|
|
18
|
-
### Navigation Functions
|
|
19
|
-
```typescript
|
|
20
|
-
import * as route from 'aberdeen/route';
|
|
21
|
-
|
|
22
|
-
route.go('/users/42'); // Navigate to new URL
|
|
23
|
-
route.go({ p: ['users', 42], hash: 'top' }); // Object form
|
|
24
|
-
route.push({ search: { tab: 'feed' } }); // Merge into current route
|
|
25
|
-
route.back(); // Go back in history
|
|
26
|
-
route.back({ path: '/home' }); // Back to matching entry, or replace
|
|
27
|
-
route.up(); // Go up one path level
|
|
28
|
-
route.persistScroll(); // Save/restore scroll position
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Reactive Routing Example
|
|
32
|
-
```typescript
|
|
33
|
-
import * as route from 'aberdeen/route';
|
|
34
|
-
|
|
35
|
-
$(() => {
|
|
36
|
-
const [section, id] = route.current.p;
|
|
37
|
-
if (section === 'users') drawUser(id);
|
|
38
|
-
else if (section === 'settings') drawSettings();
|
|
39
|
-
else drawHome();
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Dispatcher (`aberdeen/dispatcher`)
|
|
44
|
-
|
|
45
|
-
Type-safe path segment matching for complex routing.
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
import { Dispatcher, matchRest } from 'aberdeen/dispatcher';
|
|
49
|
-
import * as route from 'aberdeen/route';
|
|
50
|
-
|
|
51
|
-
const d = new Dispatcher();
|
|
52
|
-
|
|
53
|
-
// Literal string match
|
|
54
|
-
d.addRoute('home', () => drawHome());
|
|
55
|
-
|
|
56
|
-
// Number extraction (uses built-in Number function)
|
|
57
|
-
d.addRoute('user', Number, (id) => drawUser(id));
|
|
58
|
-
|
|
59
|
-
// String extraction
|
|
60
|
-
d.addRoute('user', Number, 'post', String, (userId, postId) => {
|
|
61
|
-
drawPost(userId, postId);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Rest of path as array
|
|
65
|
-
d.addRoute('search', matchRest, (terms: string[]) => {
|
|
66
|
-
performSearch(terms);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Dispatch in reactive scope
|
|
70
|
-
$(() => {
|
|
71
|
-
if (!d.dispatch(route.current.p)) {
|
|
72
|
-
draw404();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Custom Matchers
|
|
78
|
-
```typescript
|
|
79
|
-
const uuid = (s: string) => /^[0-9a-f-]{36}$/.test(s) ? s : matchFailed;
|
|
80
|
-
d.addRoute('item', uuid, (id) => drawItem(id));
|
|
81
|
-
```
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Transitions
|
|
2
|
-
|
|
3
|
-
Animate elements entering/leaving the DOM via the `create` and `destroy` properties.
|
|
4
|
-
|
|
5
|
-
**Important:** Transitions only trigger for **top-level** elements of a scope being (re-)run. Deeply nested elements drawn as part of a larger redraw do not trigger transitions.
|
|
6
|
-
|
|
7
|
-
## Built-in Transitions
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
import { grow, shrink } from 'aberdeen/transitions';
|
|
11
|
-
|
|
12
|
-
// Apply to individual elements
|
|
13
|
-
$('div create=', grow, 'destroy=', shrink, '#Animated');
|
|
14
|
-
|
|
15
|
-
// Common with onEach for list animations
|
|
16
|
-
onEach(items, item => {
|
|
17
|
-
$('li create=', grow, 'destroy=', shrink, `#${item.text}`);
|
|
18
|
-
});
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- `grow`: Scales element from 0 to full size with margin animation
|
|
22
|
-
- `shrink`: Scales element to 0 and removes from DOM after animation
|
|
23
|
-
|
|
24
|
-
Both detect horizontal flex containers and animate width instead of height.
|
|
25
|
-
|
|
26
|
-
## CSS-Based Transitions
|
|
27
|
-
|
|
28
|
-
For custom transitions, use CSS class strings (dot-separated):
|
|
29
|
-
```typescript
|
|
30
|
-
const fadeStyle = insertCss({
|
|
31
|
-
transition: 'all 0.3s ease-out',
|
|
32
|
-
'&.hidden': { opacity: 0, transform: 'translateY(-10px)' }
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Class added briefly on create (removed after layout)
|
|
36
|
-
// Class added on destroy (element removed after 2s delay)
|
|
37
|
-
$('div', fadeStyle, 'create=.hidden destroy=.hidden#Fading element');
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Custom Transition Functions
|
|
41
|
-
|
|
42
|
-
For full control, pass functions. For `destroy`, your function must remove the element:
|
|
43
|
-
```typescript
|
|
44
|
-
$('div create=', (el: HTMLElement) => {
|
|
45
|
-
// Animate on mount - element already in DOM
|
|
46
|
-
el.animate([{ opacity: 0 }, { opacity: 1 }], 300);
|
|
47
|
-
}, 'destroy=', (el: HTMLElement) => {
|
|
48
|
-
// YOU must remove the element when done
|
|
49
|
-
el.animate([{ opacity: 1 }, { opacity: 0 }], 300)
|
|
50
|
-
.finished.then(() => el.remove());
|
|
51
|
-
}, '#Custom animated');
|
|
52
|
-
```
|