aberdeen 1.6.0 → 1.7.1
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 +22 -22
- package/dist/aberdeen.d.ts +173 -129
- package/dist/aberdeen.js +188 -95
- 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 +62 -20
- 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 +1 -1
- package/skill/SKILL.md +286 -76
- package/skill/aberdeen.md +219 -203
- package/skill/dispatcher.md +16 -13
- package/skill/prediction.md +3 -3
- package/skill/route.md +44 -16
- package/skill/transitions.md +3 -3
- package/src/aberdeen.ts +403 -237
- package/src/dispatcher.ts +16 -13
- package/src/route.ts +90 -19
package/skill/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ Aberdeen is a reactive UI library using fine-grained reactivity via JavaScript P
|
|
|
12
12
|
3. **Pass observables directly** - Use `text=', ref(obj, 'key')` to avoid parent scope subscriptions
|
|
13
13
|
4. **Use `onEach` for lists** - Never iterate proxy arrays with `for`/`map` in render functions
|
|
14
14
|
5. **Class instances are great** - Better than plain objects for typed, structured state
|
|
15
|
-
6. **CSS shortcuts** - Use
|
|
15
|
+
6. **CSS shortcuts** - Use $3, $4 for spacing (1rem, 2rem), $primary for colors
|
|
16
16
|
7. **Minimal scopes** - Smaller reactive scopes = fewer DOM updates
|
|
17
17
|
|
|
18
18
|
# Obtaining info
|
|
@@ -21,7 +21,7 @@ The complete tutorial follows below. For detailed API reference open these files
|
|
|
21
21
|
|
|
22
22
|
- **[aberdeen](aberdeen.md)** - Core: `$`, `proxy`, `onEach`, `ref`, `derive`, `map`, `multiMap`, `partition`, `count`, `isEmpty`, `peek`, `dump`, `clean`, `insertCss`, `insertGlobalCss`, `mount`, `runQueue`, `darkMode`
|
|
23
23
|
- **[route](route.md)** - Routing: `current`, `go`, `push`, `back`, `up`, `persistScroll`
|
|
24
|
-
- **[dispatcher](dispatcher.md)** - Path matching: `Dispatcher`, `
|
|
24
|
+
- **[dispatcher](dispatcher.md)** - Path matching: `Dispatcher`, `MATCH_REST`, `MATCH_FAILED`
|
|
25
25
|
- **[transitions](transitions.md)** - Animations: `grow`, `shrink`
|
|
26
26
|
- **[prediction](prediction.md)** - Optimistic UI: `applyPrediction`, `applyCanon`
|
|
27
27
|
|
|
@@ -82,30 +82,26 @@ Note that the example is interactive - try typing something!
|
|
|
82
82
|
|
|
83
83
|
## Inline styles
|
|
84
84
|
|
|
85
|
-
To set inline CSS styles on elements, use the `property:value` or `property
|
|
85
|
+
To set inline CSS styles on elements, use the `property:value` (short form) or `property: value containing spaces;` (long form) syntax:
|
|
86
86
|
|
|
87
87
|
```javascript
|
|
88
|
-
$('
|
|
88
|
+
$('p color:red padding:8px background-color:#a882 border: 2px solid #a884; #Styled text');
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
## CSS shortcuts
|
|
92
|
-
|
|
93
|
-
Aberdeen provides shortcuts for commonly used CSS properties, making your code more concise.
|
|
94
|
-
|
|
95
91
|
### Property shortcuts
|
|
96
92
|
|
|
97
|
-
|
|
93
|
+
Aberdeen provides shortcuts for commonly used CSS properties, making your code more concise.
|
|
98
94
|
|
|
99
95
|
| Shortcut | Expands to |
|
|
100
96
|
|----------|------------|
|
|
101
|
-
| `m`, `mt`, `mb`, `ml`, `mr` | `margin`, `
|
|
97
|
+
| `m`, `mt`, `mb`, `ml`, `mr` | `margin`, `margin-top`, `margin-bottom`, `margin-left`, `margin-right` |
|
|
102
98
|
| `mv`, `mh` | Vertical (top+bottom) or horizontal (left+right) margins |
|
|
103
|
-
| `p`, `pt`, `pb`, `pl`, `pr` | `padding`, `
|
|
99
|
+
| `p`, `pt`, `pb`, `pl`, `pr` | `padding`, `padding-top`, `padding-bottom`, `padding-left`, `padding-right` |
|
|
104
100
|
| `pv`, `ph` | Vertical or horizontal padding |
|
|
105
101
|
| `w`, `h` | `width`, `height` |
|
|
106
102
|
| `bg` | `background` |
|
|
107
103
|
| `fg` | `color` |
|
|
108
|
-
| `r` | `
|
|
104
|
+
| `r` | `border-radius` |
|
|
109
105
|
|
|
110
106
|
```javascript
|
|
111
107
|
$('div mv:10px ph:20px bg:lightblue r:10% #Styled box');
|
|
@@ -130,7 +126,8 @@ $('button bg:$danger fg:$textLight #Danger');
|
|
|
130
126
|
|
|
131
127
|
The above generates CSS like `background: var(--primary)` and automatically injects a `:root` style defining the actual values. Since this uses native CSS custom properties, changes to `cssVars` automatically propagate to all elements using those values.
|
|
132
128
|
|
|
133
|
-
|
|
129
|
+
### Spacing variables
|
|
130
|
+
|
|
134
131
|
You can optionally initialize `cssVars` with keys `1` through `12` mapping to an exponential `rem` scale using {@link aberdeen.setSpacingCssVars}. Since CSS custom property names can't start with a digit, numeric keys are prefixed with `m` (e.g., `$3` becomes `var(--m3)`):
|
|
135
132
|
|
|
136
133
|
```javascript
|
|
@@ -367,7 +364,7 @@ const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0
|
|
|
367
364
|
|
|
368
365
|
$('button text="Add item" click=', () => pairs[randomWord()] = randomWord());
|
|
369
366
|
|
|
370
|
-
$('div.row.wide
|
|
367
|
+
$('div.row.wide margin-top:1em', () => {
|
|
371
368
|
$('div.box#By key', () => {
|
|
372
369
|
onEach(pairs, (value, key) => {
|
|
373
370
|
$(`li#${key}: ${value}`)
|
|
@@ -419,22 +416,25 @@ $('div.box', () => {
|
|
|
419
416
|
## CSS
|
|
420
417
|
Through the {@link aberdeen.insertCss} function, Aberdeen provides a way to create component-local CSS.
|
|
421
418
|
|
|
419
|
+
For simple single-element styles, you can pass a string directly:
|
|
420
|
+
|
|
422
421
|
```javascript
|
|
423
|
-
import { $,
|
|
422
|
+
import { $, insertCss } from 'aberdeen';
|
|
423
|
+
|
|
424
|
+
const simpleCard = insertCss("bg:#f0f0f0 p:$3 r:8px");
|
|
425
|
+
$('div', simpleCard, '#Card content');
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
For more complex styles with nested selectors, pass an object where each key is a selector and each value is a style string using the same `property:value` syntax as inline styles:
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
import { $, insertCss } from 'aberdeen';
|
|
424
432
|
|
|
425
433
|
// Create a CSS class that can be applied to elements
|
|
426
434
|
const myBoxStyle = insertCss({
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
button:
|
|
430
|
-
backgroundColor: '#6936cd',
|
|
431
|
-
border: 0,
|
|
432
|
-
transition: 'box-shadow 0.3s',
|
|
433
|
-
boxShadow: '0 0 4px #ff6a0044',
|
|
434
|
-
'&:hover': {
|
|
435
|
-
boxShadow: '0 0 16px #ff6a0088',
|
|
436
|
-
}
|
|
437
|
-
}
|
|
435
|
+
"&": "border-color:#6936cd background-color:#1b0447",
|
|
436
|
+
"button": "background-color:#6936cd border:0 transition: box-shadow 0.3s; box-shadow: 0 0 4px #ff6a0044;",
|
|
437
|
+
"button:hover": "box-shadow: 0 0 16px #ff6a0088;"
|
|
438
438
|
});
|
|
439
439
|
|
|
440
440
|
// myBoxStyle is now something like ".AbdStl1", the name for a generated CSS class.
|
|
@@ -442,7 +442,9 @@ const myBoxStyle = insertCss({
|
|
|
442
442
|
$('div.box', myBoxStyle, 'button#Click me');
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
The `"&"` selector refers to the element with the generated class itself. Child selectors like `"button"` are scoped to descendants of that element, while pseudo-selectors like `"&:hover"` apply to the element itself.
|
|
446
|
+
|
|
447
|
+
This allows you to create single-file components with advanced CSS rules. The {@link aberdeen.insertGlobalCss} function can be used to add CSS without a class prefix - it accepts the same string or object syntax.
|
|
446
448
|
|
|
447
449
|
Both functions support the same CSS shortcuts and variables as inline styles (see above). For example:
|
|
448
450
|
|
|
@@ -450,13 +452,8 @@ Both functions support the same CSS shortcuts and variables as inline styles (se
|
|
|
450
452
|
import { cssVars, insertGlobalCss } from 'aberdeen';
|
|
451
453
|
cssVars.boxBg = '#f0f0e0';
|
|
452
454
|
insertGlobalCss({
|
|
453
|
-
body:
|
|
454
|
-
|
|
455
|
-
},
|
|
456
|
-
form: {
|
|
457
|
-
bg: "$boxBg", // Using background shortcut and CSS variable
|
|
458
|
-
mv: "$3", // Set vertical margin to predefined spacing value $3 (1rem)
|
|
459
|
-
}
|
|
455
|
+
"body": "m:0", // Using shortcut for margin
|
|
456
|
+
"form": "bg:$boxBg mv:$3" // Using background shortcut, CSS variable, and spacing value
|
|
460
457
|
});
|
|
461
458
|
```
|
|
462
459
|
|
|
@@ -467,27 +464,20 @@ Aberdeen allows you to easily apply transitions on element creation and element
|
|
|
467
464
|
|
|
468
465
|
```javascript
|
|
469
466
|
let titleStyle = insertCss({
|
|
470
|
-
transition:
|
|
471
|
-
|
|
472
|
-
"&.
|
|
473
|
-
|
|
474
|
-
},
|
|
475
|
-
"&.imploded": {
|
|
476
|
-
transform: "scale(0.1)",
|
|
477
|
-
},
|
|
478
|
-
"&.exploded": {
|
|
479
|
-
transform: "scale(5)",
|
|
480
|
-
},
|
|
467
|
+
"&": "transition: all 1s ease-out; transform-origin: left center;",
|
|
468
|
+
"&.faded": "opacity:0",
|
|
469
|
+
"&.imploded": "transform:scale(0.1)",
|
|
470
|
+
"&.exploded": "transform:scale(5)"
|
|
481
471
|
});
|
|
482
472
|
|
|
483
473
|
const show = proxy(true);
|
|
484
474
|
$('label', () => {
|
|
485
|
-
$('input
|
|
475
|
+
$('input type=checkbox bind=', show);
|
|
486
476
|
$('#Show title');
|
|
487
477
|
});
|
|
488
478
|
$(() => {
|
|
489
479
|
if (!show.value) return;
|
|
490
|
-
$('h2#(Dis)appearing text', titleStyle, 'create
|
|
480
|
+
$('h2#(Dis)appearing text', titleStyle, 'create=faded.imploded destroy=faded.exploded');
|
|
491
481
|
});
|
|
492
482
|
```
|
|
493
483
|
|
|
@@ -542,46 +532,25 @@ const data = proxy({ a: 1, b: 2 });
|
|
|
542
532
|
$(() => {
|
|
543
533
|
// This scope only re-runs when data.a changes
|
|
544
534
|
// Changes to data.b won't trigger a re-render
|
|
545
|
-
|
|
546
|
-
console.log(`A is ${data.a}, B was ${b} when A changed.`);
|
|
535
|
+
$(`h2#a == ${data.a} && b == ${peek(data, 'b')}`);
|
|
547
536
|
});
|
|
548
537
|
|
|
549
|
-
$(
|
|
550
|
-
$(
|
|
538
|
+
$(`button text="a++ (will update)" click=`, () => data.a++);
|
|
539
|
+
$(`button ml:1rem text="b++ (won't update)" click=`, () => data.b++);
|
|
551
540
|
```
|
|
552
541
|
|
|
553
542
|
You can also pass a function to `peek()` to execute it without any subscriptions:
|
|
554
543
|
|
|
555
544
|
```javascript
|
|
556
|
-
const
|
|
545
|
+
const a = proxy(42);
|
|
546
|
+
const b = proxy(7);
|
|
547
|
+
const sum = peek(() => a.value + b.value); // Reads both without subscribing
|
|
548
|
+
$('#Sum is: '+sum);
|
|
549
|
+
setInterval(() => a.value++, 1000); // Won't update
|
|
557
550
|
```
|
|
558
551
|
|
|
559
552
|
This can be useful to avoid rerenders (of even rerender loops) when you only need a point-in-time snapshot of some reactive data.
|
|
560
553
|
|
|
561
|
-
## Debugging with dump()
|
|
562
|
-
|
|
563
|
-
The {@link aberdeen.dump} function creates a live, interactive tree view of any data structure in the DOM. It's particularly useful for debugging reactive state:
|
|
564
|
-
|
|
565
|
-
```javascript
|
|
566
|
-
import { $, proxy, dump } from 'aberdeen';
|
|
567
|
-
|
|
568
|
-
const state = proxy({
|
|
569
|
-
user: { name: 'Frank', kids: 1 },
|
|
570
|
-
items: ['a', 'b']
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
$('h2#Live State Dump');
|
|
574
|
-
dump(state);
|
|
575
|
-
|
|
576
|
-
// The dump updates automatically as state changes
|
|
577
|
-
$('button text="Update state" click=', () => {
|
|
578
|
-
state.user.kids++;
|
|
579
|
-
state.items.push('new');
|
|
580
|
-
});
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
The dump renders recursively using `<ul>` and `<li>` elements, showing all properties and their values. It updates reactively when any proxied data changes. It is intended for debugging, though with some CSS styling you may find it useful in some simple real-world scenarios as well.
|
|
584
|
-
|
|
585
554
|
## Derived values
|
|
586
555
|
An observer scope doesn't *need* to create DOM elements. It may also perform other side effects, such as modifying other observable objects. For instance:
|
|
587
556
|
|
|
@@ -645,6 +614,31 @@ $(() => {
|
|
|
645
614
|
})
|
|
646
615
|
```
|
|
647
616
|
|
|
617
|
+
## Debugging with dump()
|
|
618
|
+
|
|
619
|
+
The {@link aberdeen.dump} function creates a live, interactive tree view of any data structure in the DOM. It's particularly useful for debugging reactive state:
|
|
620
|
+
|
|
621
|
+
```javascript
|
|
622
|
+
import { $, proxy, dump } from 'aberdeen';
|
|
623
|
+
|
|
624
|
+
const state = proxy({
|
|
625
|
+
user: { name: 'Frank', kids: 1 },
|
|
626
|
+
items: ['a', 'b']
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
$('h2#Live State Dump');
|
|
630
|
+
dump(state);
|
|
631
|
+
|
|
632
|
+
// The dump updates automatically as state changes
|
|
633
|
+
$('button text="Update state" click=', () => {
|
|
634
|
+
state.user.kids++;
|
|
635
|
+
state.items.push('new');
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
The dump renders recursively using `<ul>` and `<li>` elements, showing all properties and their values. It updates reactively when any proxied data changes. It is intended for debugging, though with some CSS styling you may find it useful in some simple real-world scenarios as well.
|
|
640
|
+
|
|
641
|
+
|
|
648
642
|
## html-to-aberdeen
|
|
649
643
|
|
|
650
644
|
Sometimes, you want to just paste a largish block of HTML into your application (and then maybe modify it to bind some actual data). Having to translate HTML to `$` calls manually is little fun, so there's a tool for that:
|
|
@@ -657,6 +651,222 @@ It takes HTML on stdin (paste it and press `ctrl-d` for end-of-file), and output
|
|
|
657
651
|
|
|
658
652
|
> Caveat: This tool has been vibe coded (thanks Claude!) with very little code review. As it doesn't use the filesystem nor the network, I'd say it's safe to use though! :-) Also, it happens to work pretty well.
|
|
659
653
|
|
|
654
|
+
## Routing
|
|
655
|
+
|
|
656
|
+
Aberdeen provides an optional built-in router via the {@link route} module. The router is reactive and integrates seamlessly with browser history.
|
|
657
|
+
|
|
658
|
+
The {@link route.current} object is an observable that reflects the current URL:
|
|
659
|
+
|
|
660
|
+
```javascript
|
|
661
|
+
import { $ } from 'aberdeen';
|
|
662
|
+
import * as route from 'aberdeen/route';
|
|
663
|
+
|
|
664
|
+
$(() => {
|
|
665
|
+
$(`p#Path string: ${route.current.path}`); // eg "/example/123"
|
|
666
|
+
$(`p#Path segments: ${JSON.stringify(route.current.p)}`); // eg ["example", "123"]
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
To navigate programmatically, use {@link route.go}:
|
|
671
|
+
|
|
672
|
+
```javascript
|
|
673
|
+
import { $ } from 'aberdeen';
|
|
674
|
+
import * as route from 'aberdeen/route';
|
|
675
|
+
console.log('pn', location.protocol, location.host, location.hostname, location.pathname);
|
|
676
|
+
|
|
677
|
+
$('button#Go to settings', {
|
|
678
|
+
click: () => route.go('/settings')
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Or using path segments
|
|
682
|
+
$('button ml:1rem #Go to user 123', {
|
|
683
|
+
click: () => route.go({p: ['users', 123]})
|
|
684
|
+
});
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
For convenience, you can call {@link route.interceptLinks} once to automatically convert clicks on local `<a>` tags into Aberdeen routing, so you can use regular anchor tags without manual click handlers. Example: `$('a href=/settings text=Settings')`.
|
|
688
|
+
|
|
689
|
+
```javascript
|
|
690
|
+
import { $ } from 'aberdeen';
|
|
691
|
+
import * as route from 'aberdeen/route';
|
|
692
|
+
|
|
693
|
+
route.interceptLinks(); // Just once on startup:
|
|
694
|
+
|
|
695
|
+
$('a role=button href=/settings #Go to settings')
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
The {@link route.push} function is useful for overlays that should be closeable with browser back:
|
|
699
|
+
|
|
700
|
+
```javascript
|
|
701
|
+
import { $ } from 'aberdeen';
|
|
702
|
+
import * as route from 'aberdeen/route';
|
|
703
|
+
|
|
704
|
+
$('button#Open modal', {
|
|
705
|
+
click: () => route.push({state: {modal: 'settings'}})
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
$(() => {
|
|
709
|
+
if (!route.current.state.modal) return;
|
|
710
|
+
$('div.modal-overlay', {
|
|
711
|
+
click: () => route.back({state: {modal: undefined}})
|
|
712
|
+
}, () => {
|
|
713
|
+
$('div.modal#Modal content here');
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Optionally, you can use the {@link dispatcher.Dispatcher} class for declarative routing. It allows you to register route patterns with associated handler functions, which are invoked when the current route matches the pattern. It can match typed parameters and rest parameters.
|
|
719
|
+
|
|
720
|
+
## Prediction
|
|
721
|
+
|
|
722
|
+
When building interactive applications with client-server communication, Aberdeen's prediction system allows for optimistic UI updates. The {@link prediction.applyPrediction} function records changes to any proxied objects made within its callback. These changes are treated as *predictions* that may later be confirmed or reverted based on server responses. When a server response arrives, the {@link prediction.applyCanon} function applies authoritative changes from the server, reverting any conflicting predictions while attempting to reapply non-conflicting ones.
|
|
723
|
+
|
|
724
|
+
## Full Example: Multi-page App
|
|
725
|
+
|
|
726
|
+
Here's a complete example (a contact manager) demonstrating routing, state management, CSS, dark mode, and dynamic content:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
import { $, proxy, onEach, cssVars, ref, darkMode, insertCss, insertGlobalCss, setSpacingCssVars, map } from 'aberdeen';
|
|
730
|
+
import * as route from 'aberdeen/route';
|
|
731
|
+
import { Dispatcher } from 'aberdeen/dispatcher';
|
|
732
|
+
import { grow, shrink } from 'aberdeen/transitions';
|
|
733
|
+
|
|
734
|
+
class Contact {
|
|
735
|
+
constructor(
|
|
736
|
+
public id: number,
|
|
737
|
+
public firstName: string,
|
|
738
|
+
public lastName: string,
|
|
739
|
+
public email: string,
|
|
740
|
+
public phone: string
|
|
741
|
+
) {}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Enable link interception for SPA navigation
|
|
745
|
+
route.interceptLinks();
|
|
746
|
+
|
|
747
|
+
// Initialize $1-$12 CSS variables for consistent spacing ($2=0.5rem, $3=1rem, $4=2rem, etc.)
|
|
748
|
+
setSpacingCssVars();
|
|
749
|
+
|
|
750
|
+
// Reactive theme based on system preference
|
|
751
|
+
$(() => {
|
|
752
|
+
cssVars.primary = '#2563eb';
|
|
753
|
+
cssVars.bg = darkMode() ? '#0f172a' : '#ffffff';
|
|
754
|
+
cssVars.fg = darkMode() ? '#e2e8f0' : '#1e293b';
|
|
755
|
+
cssVars.cardBg = darkMode() ? '#1e293b' : '#f8fafc';
|
|
756
|
+
cssVars.border = darkMode() ? '#334155' : '#e2e8f0';
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Global styles for semantic HTML elements that apply everywhere
|
|
760
|
+
insertGlobalCss({
|
|
761
|
+
"*": "m:0 p:0",
|
|
762
|
+
"body": "bg:$bg fg:$fg font-family: system-ui, sans-serif;",
|
|
763
|
+
"a": "color:$primary text-decoration:none",
|
|
764
|
+
"a:hover": "text-decoration:underline",
|
|
765
|
+
"a[role=button]": "bg:$primary fg:white r:8px p:$2",
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Application state
|
|
769
|
+
const contacts = proxy([
|
|
770
|
+
new Contact(1, 'Emma', 'Wilson', 'emma.wilson@email.com', '555-0101'),
|
|
771
|
+
new Contact(2, 'James', 'Anderson', 'j.anderson@email.com', '555-0102'),
|
|
772
|
+
new Contact(3, 'Sofia', 'Martinez', 'sofia.m@email.com', '555-0103'),
|
|
773
|
+
new Contact(4, 'Liam', 'Brown', 'liam.brown@email.com', '555-0104')
|
|
774
|
+
]);
|
|
775
|
+
|
|
776
|
+
// Router setup
|
|
777
|
+
const dispatcher = new Dispatcher();
|
|
778
|
+
dispatcher.addRoute(drawHome);
|
|
779
|
+
dispatcher.addRoute('contacts', drawContactList);
|
|
780
|
+
dispatcher.addRoute('contacts', Number, drawContactDetail);
|
|
781
|
+
|
|
782
|
+
// Main app
|
|
783
|
+
$('div.app', () => {
|
|
784
|
+
$('nav display:flex gap:$3 p:$3 border-bottom: 1px solid $border;', () => {
|
|
785
|
+
$('a href=/ text=Home font-weight:', route.current.p.length === 0 ? 'bold' : 'normal');
|
|
786
|
+
$('a href=/contacts text=Contacts font-weight:', route.current.p[0] === 'contacts' ? 'bold' : 'normal');
|
|
787
|
+
});
|
|
788
|
+
$('main p:$3', () => dispatcher.dispatch(route.current.p));
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
function drawHome() {
|
|
792
|
+
$('h1#Contact Manager');
|
|
793
|
+
$('p#A modern contact list with search, sort, and dark mode support.');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Contact card styles
|
|
797
|
+
const cardStyle = insertCss({
|
|
798
|
+
"&": "bg:$cardBg border: 1px solid $border; r:8px p:$3 mv:$2 display:block transition: transform 0.2s;",
|
|
799
|
+
"&:hover": "transform:translateX(4px)",
|
|
800
|
+
"a&": "color:inherit;",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const filterStyle = insertCss({
|
|
804
|
+
"&": "display:flex gap:$3 mv:$3",
|
|
805
|
+
"> *": "p:$2 r:4px bg:$bg fg:$fg border: 1px solid $border;",
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
function drawContactList() {
|
|
809
|
+
$('h1#Contacts');
|
|
810
|
+
|
|
811
|
+
// Search and sort controls
|
|
812
|
+
$('div', filterStyle, () => {
|
|
813
|
+
$('input flex:1 placeholder="Search contacts..." bind=', ref(route.current.search, 'q'));
|
|
814
|
+
$('select bind=', ref(route.current.search, 'sort'), () => {
|
|
815
|
+
$('option value=firstName #First Name');
|
|
816
|
+
$('option value=lastName #Last Name');
|
|
817
|
+
$('option value=email #Email');
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// Contact list
|
|
822
|
+
$('div', () => {
|
|
823
|
+
const sortBy = route.current.search.sort || 'firstName';
|
|
824
|
+
|
|
825
|
+
const filtered = map(contacts, contact => {
|
|
826
|
+
const query = route.current.search.q;
|
|
827
|
+
if (query) {
|
|
828
|
+
const info = `${contact.firstName} ${contact.lastName} ${contact.email}`;
|
|
829
|
+
if (!info.toLowerCase().includes(query.toLowerCase())) return; // Skip!
|
|
830
|
+
}
|
|
831
|
+
return contact;
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
onEach(filtered, contact => {
|
|
835
|
+
$('a', cardStyle, 'create=', grow, 'destroy=', shrink, `href=/contacts/${contact.id}`, () => {
|
|
836
|
+
$('h2', () => {
|
|
837
|
+
$('span font-weight:normal text=', contact.firstName+" ");
|
|
838
|
+
$('span text=', contact.lastName);
|
|
839
|
+
});
|
|
840
|
+
$('div text=', contact.email);
|
|
841
|
+
});
|
|
842
|
+
}, contact => contact[sortBy].toLowerCase());
|
|
843
|
+
|
|
844
|
+
$(`a role=button mt:$3 text="Add new contact" href=/contacts/${contacts.length}`);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Detail form styles
|
|
849
|
+
const detailStyle = insertCss({
|
|
850
|
+
"&": "bg:$cardBg border: 1px solid $border; r:8px p:$4 max-width:600px",
|
|
851
|
+
"label": "display:block font-weight:600 mt:$3 mb:$2",
|
|
852
|
+
"input": "w:100% p:$2 r:4px border: 1px solid $border; bg:$bg fg:$fg"
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
function drawContactDetail(id: number) {
|
|
856
|
+
const contact = contacts[id] ||= {};
|
|
857
|
+
|
|
858
|
+
$('a role=button href=/contacts #← Back');
|
|
859
|
+
|
|
860
|
+
$('div mt:$3', detailStyle, () => {
|
|
861
|
+
$('h2 mb:$2 text=', ref(contact, 'firstName'), 'text=', ' ', 'text=', ref(contact, 'lastName'));
|
|
862
|
+
$('label text="First Name" input bind=', ref(contact, 'firstName'));
|
|
863
|
+
$('label text="Last Name" input bind=', ref(contact, 'lastName'));
|
|
864
|
+
$('label text="Email" input type=email bind=', ref(contact, 'email'));
|
|
865
|
+
$('label text="Phone" input type=tel bind=', ref(contact, 'phone'));
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
660
870
|
## Further reading
|
|
661
871
|
|
|
662
872
|
If you've understood all/most of the above, you should be ready to get going with Aberdeen! You may also find these links helpful:
|