Skip to content

Commit e8418b8

Browse files
authored
Merge pull request #6 from AegisJSProject/feature/skip-hydration
Update `toString()` on signals to avoid need for hydration step
2 parents 19a0dff + 608c18a commit e8418b8

File tree

10 files changed

+343
-32
lines changed

10 files changed

+343
-32
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v1.0.4] - 2026-03-15
11+
12+
### Changed
13+
- Update `toString()` on signals to avoid need for hydration step
14+
1015
## [v1.0.3] - 2026-03-14
1116

1217
### Added

attr.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1+
import { escapeHTML } from '@aegisjsproject/escape/html.js';
12
import { DisposableComputed, DisposableState } from './disposable.js';
2-
3-
export const SIGNAL_DATA_ATTR = 'data-attr-signal';
4-
export const SIGNAL_DATA_ATTR_SELECTOR = `[${SIGNAL_DATA_ATTR}]`;
3+
import { SIGNAL_DATA_ATTR } from './consts.js';
54

65
export class AttrState extends DisposableState {
76
#name;
@@ -25,7 +24,15 @@ export class AttrState extends DisposableState {
2524
}
2625

2726
toString() {
28-
return `${SIGNAL_DATA_ATTR}="${this.ref}"`;
27+
const val = this.get();
28+
29+
if (Array.isArray(val)) {
30+
return `${SIGNAL_DATA_ATTR}="${this.ref}" ${this.#name}="${escapeHTML(val.join(' '))}"`;
31+
} else if (val === false) {
32+
return `${SIGNAL_DATA_ATTR}="${this.ref}"`;
33+
} else {
34+
return `${SIGNAL_DATA_ATTR}="${this.ref}" ${this.#name}="${escapeHTML(val)}"`;
35+
}
2936
}
3037
}
3138

@@ -50,7 +57,15 @@ export class AttrComputed extends DisposableComputed {
5057
}
5158

5259
toString() {
53-
return `${SIGNAL_DATA_ATTR}="${this.ref}"`;
60+
const val = this.get();
61+
62+
if (Array.isArray(val)) {
63+
return `${SIGNAL_DATA_ATTR}="${this.ref}" ${this.#name}="${escapeHTML(val.join(' '))}"`;
64+
} else if (val === false) {
65+
return `${SIGNAL_DATA_ATTR}="${this.ref}"`;
66+
} else {
67+
return `${SIGNAL_DATA_ATTR}="${this.ref}" ${this.#name}="${escapeHTML(val)}"`;
68+
}
5469
}
5570
}
5671

consts.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const NOOP = () => undefined;
2+
export const ZERO_WIDTH_SPACE = '\u200B';
3+
export const SIGNAL_DATA_ATTR = 'data-attr-signal';
4+
export const SIGNAL_DATA_ATTR_SELECTOR = `[${SIGNAL_DATA_ATTR}]`;

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ document.adoptedStyleSheets = [properties, theme, misc, forms, btn];
1515
const stack = new DisposableStack();
1616
const controller = stack.adopt(new AbortController(), controller => controller.abort());
1717
const signal = registerSignal(controller.signal);
18-
const $name = stack.use($text('Silly person'));
18+
const $name = stack.use($text('Silly person <script>alert("1")</script>'));
1919
const $nameAttr = stack.use($data('user-name', () => $name.get()));
2020
const $isHidden = stack.use($hidden(false));
2121
const $desc = stack.use($aria('description', () => `Description for ${$name.get()}.`));

list.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { DisposableState, DisposableComputed } from './disposable.js';
2+
3+
const ZERO_WIDTH_SPACE = '\u200B';
4+
5+
export class ListComputed extends DisposableComputed {
6+
static REF_PREFIX = '__list_signal';
7+
8+
constructor(callback, config) {
9+
super(() => {
10+
const value = callback();
11+
12+
if (Array.isArray(value)) {
13+
return value;
14+
} else if (typeof value[Symbol.iterator] === 'function') {
15+
return Array.from(value);
16+
} else {
17+
return [value];
18+
}
19+
}, config);
20+
}
21+
22+
map(cb) {
23+
return new ListComputed(cb);
24+
}
25+
26+
toString() {
27+
return `${ZERO_WIDTH_SPACE}<!--${this.ref}-->`;
28+
}
29+
}
30+
31+
export class ListState extends DisposableState {
32+
constructor(value, config) {
33+
if (Array.isArray(value)) {
34+
super(value, config);
35+
} else if (typeof value[Symbol.iterator] === 'function') {
36+
super(Array.from(value), config);
37+
} else {
38+
super([value], config);
39+
}
40+
}
41+
42+
map(cb) {
43+
return new ListComputed(cb);
44+
}
45+
46+
toString() {
47+
return `${ZERO_WIDTH_SPACE}<!--${this.ref}-->`;
48+
}
49+
}
50+
51+
export const $list = (val, config) => typeof val === 'function'
52+
? new ListComputed(val, config)
53+
: new ListState(val, config);

package-lock.json

Lines changed: 34 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aegisjsproject/iota",
3-
"version": "1.0.3",
3+
"version": "1.0.4",
44
"description": "A Signals-based reactivity library",
55
"keywords": [
66
"signals",
@@ -76,6 +76,7 @@
7676
},
7777
"homepage": "https://github.com/AegisJSProject/iota#readme",
7878
"dependencies": {
79+
"@aegisjsproject/escape": "^1.0.4",
7980
"@shgysk8zer0/signals": "^0.0.3"
8081
},
8182
"devDependencies": {
@@ -84,7 +85,7 @@
8485
"@rollup/plugin-terser": "^1.0.0",
8586
"@shgysk8zer0/eslint-config": "^1.0.7",
8687
"@shgysk8zer0/http-server": "^1.1.1",
87-
"@shgysk8zer0/importmap": "^1.8.2",
88+
"@shgysk8zer0/importmap": "^1.8.3",
8889
"eslint": "^10.0.3",
8990
"rollup": "^4.59.0"
9091
}

0 commit comments

Comments
 (0)