diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27947cf..17a8416 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [v1.0.2] - 2026-03-13
+
+### Added
+- Add support for boolean attributes
+- Add `$render()` (observer & replace children of target)
+
## [v1.0.1] - 2026-03-11
### Added
diff --git a/index.html b/index.html
index 3910122..dfbf42f 100644
--- a/index.html
+++ b/index.html
@@ -16,6 +16,7 @@
+
Request
diff --git a/index.js b/index.js
index e2054cf..9f222ef 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
-import { $text, $attr, $observe } from '@aegisjsproject/iota';
+import { $text, $attr, $render } from '@aegisjsproject/iota';
import { createHTMLParser } from '@aegisjsproject/core/parsers/html.js';
-import { onInput, observeEvents, createCallback } from '@aegisjsproject/callback-registry';
+import { onInput, onClick, observeEvents, createCallback, signal as signalAttr, registerSignal } from '@aegisjsproject/callback-registry';
import { sanitizer as sanitizerConfig } from '@aegisjsproject/sanitizer/config/base.js';
// import pkg from '/package.json' with { type: 'json' }; // Wrong mime-type causes error
import properties from '@aegisjsproject/styles/css/properties.css' with { type: 'css' };
@@ -8,11 +8,12 @@ import theme from '@aegisjsproject/styles/css/theme.css' with { type: 'css' };
import misc from '@aegisjsproject/styles/css/misc.css' with { type: 'css' };
import forms from '@aegisjsproject/styles/css/forms.css' with { type: 'css' };
import btn from '@aegisjsproject/styles/css/button.css' with { type: 'css' };
+import './reactive-element.js';
// document.title = pkg.name;
document.adoptedStyleSheets = [properties, theme, misc, forms, btn];
-// Need to force-allow comments for text signals
+// Need to force-allow comments for text signals until change is made in lib
const html = createHTMLParser({
...sanitizerConfig,
comments: true,
@@ -20,21 +21,36 @@ const html = createHTMLParser({
const stack = new DisposableStack();
const controller = stack.adopt(new AbortController(), controller => controller.abort());
+const signal = registerSignal(controller.signal);
const $name = stack.use($text('Silly person'));
-const $value = stack.use($attr('value', () => $name.get()));
+const $hidden = stack.use($attr('hidden', false));
+const $open = stack.use($attr('open', false));
+const $disabled = $attr('disabled', false);
+const $script = $text('alert(location.href)');
+const toggleOpen = createCallback(() => $open.set(! $open.get()));
+const toggleHidden = createCallback(() => $hidden.set(! $hidden.get()));
+stack.defer(() => $disabled.set(true));
-document.getElementById('main').prepend(html`Hello, ${$name}!
+$render(html`
+ Hello, ${$name}!
+
-`);
+
+
+
+`, 'container');
document.documentElement.addEventListener('command', ({ source, command }) => {
if (command === '--dispose') {
@@ -43,11 +59,5 @@ document.documentElement.addEventListener('command', ({ source, command }) => {
}
}, { signal: controller.signal });
-stack.defer(() => {
- for (const el of document.forms.container.elements) {
- el.disabled = true;
- }
-});
-
-$observe();
+// This is called for an external library.
observeEvents();
diff --git a/iota.js b/iota.js
index 9710304..a86bfd5 100644
--- a/iota.js
+++ b/iota.js
@@ -3,7 +3,7 @@ export { registerSignal, getSignalFromRef, hasSignalRef, unregisterSignal } from
export { DisposableComputed, DisposableState, $computed, $signal } from './disposable.js';
export {
watchSignal, unwatchSignal, unwatchSignalCallback, observeAttrSignalRefs, observeSignalRefs,
- observeTextSignalRefs, $watch, $unwatch, $observe,
+ observeTextSignalRefs, $watch, $unwatch, $observe, $render,
} from './watcher.js';
export { TextComputed, TextState, $text } from './text.js';
export { AttrComputed, AttrState, $attr } from './attr.js';
diff --git a/package-lock.json b/package-lock.json
index d69ff71..2e3ac71 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@aegisjsproject/iota",
- "version": "1.0.1",
+ "version": "1.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@aegisjsproject/iota",
- "version": "1.0.1",
+ "version": "1.0.2",
"funding": [
{
"type": "librepay",
diff --git a/package.json b/package.json
index 5179711..23d5f98 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@aegisjsproject/iota",
- "version": "1.0.1",
+ "version": "1.0.2",
"description": "A Signals-based reactivity library",
"keywords": [
"signals",
diff --git a/watcher.js b/watcher.js
index b73a790..be8b9d7 100644
--- a/watcher.js
+++ b/watcher.js
@@ -2,6 +2,8 @@ import { Signal } from '@shgysk8zer0/signals';
import { hasSignalRef, getSignalFromRef } from './registry.js';
import { SIGNAL_DATA_ATTR } from './attr.js';
+const ATTR_OWNER_KEY = Symbol('Attr:owner');
+
const noop = () => undefined;
/**
* @type {boolean}
@@ -38,6 +40,13 @@ let updating = false;
* @returns {void}
*/
+/**
+ * @typedef ObserverConfigObject
+ * @property {DisposableStack|AsyncDisposable} [stack]
+ * @property {AbortSignal} [signal]
+ * @property {DocumentOrShadowRoot} [base=document]
+ */
+
/**
* @template T
* @type {WeakMap, Set>>}
@@ -137,7 +146,12 @@ export function unwatchSignalCallback(signal, callback) {
}
}
-
+/**
+ *
+ * @param @param {DocumentOrShadowRoot|Element|DocumentFragment|string} [root=document.body]
+ * @param {ObserverConfigObject} [config]
+ * @returns {DocumentOrShadowRoot|Element|DocumentFragment}
+ */
export function observeTextSignalRefs(root = document.body, { stack, signal, base = document } = {}) {
if (typeof root === 'string') {
return observeTextSignalRefs(base.getElementById(root), { stack, signal });
@@ -163,6 +177,12 @@ export function observeTextSignalRefs(root = document.body, { stack, signal, bas
}
}
+/**
+ *
+ * @param {DocumentOrShadowRoot|Element|DocumentFragment|string} [root=document.body]
+ * @param {ObserverConfigObject} [config]
+ * @returns {DocumentOrShadowRoot|Element|DocumentFragment}
+ */
export function observeAttrSignalRefs(root = document.body, { stack, signal, base = document } = {}) {
if (typeof root === 'string') {
return observeAttrSignalRefs(base.getElementById(root), { stack, signal });
@@ -175,10 +195,28 @@ export function observeAttrSignalRefs(root = document.body, { stack, signal, bas
if (hasSignalRef(key)) {
const sig = getSignalFromRef(key);
const attr = root.ownerDocument.createAttribute(sig.name);
- attr.value = sig.get();
- el.setAttributeNode(attr);
+ const val = sig.get();
+
+ if (val !== false) {
+ attr.value = sig.get();
+ el.setAttributeNode(attr);
+ }
+
+ Object.defineProperty(attr, ATTR_OWNER_KEY, { value: el, enumerable: false, writable: false, configurable: false });
el.removeAttribute(SIGNAL_DATA_ATTR);
- watchSignal(sig, newVal => attr.value = newVal);
+
+ watchSignal(sig, newVal => {
+ if (typeof newVal === 'boolean') {
+ attr[ATTR_OWNER_KEY].toggleAttribute(attr.name, newVal);
+ } else {
+ attr.value = newVal;
+
+ if (! (attr.ownerElement instanceof Element)) {
+ attr[ATTR_OWNER_KEY].setAttributeNode(attr);
+ }
+ }
+ });
+
stack?.defer?.(sig[Symbol.dispose]?.bind?.(sig));
signal?.addEventListener?.('abort', sig[Symbol.dispose]?.bind?.(sig), { once: true });
}
@@ -188,6 +226,12 @@ export function observeAttrSignalRefs(root = document.body, { stack, signal, bas
return root;
}
+/**
+ *
+ * @param {DocumentOrShadowRoot|Element|DocumentFragment|string} [root=document.body]
+ * @param {ObserverConfigObject} [config]
+ * @returns {DocumentOrShadowRoot}
+ */
export function observeSignalRefs(root = document.body, { stack, signal, base = document } = {}) {
if (typeof root === 'string') {
return observeSignalRefs(base.getElementById(root), { stack, signal });
@@ -199,3 +243,26 @@ export function observeSignalRefs(root = document.body, { stack, signal, base =
}
export const $observe = observeSignalRefs;
+
+/**
+ *
+ * @param {Element|DocumentFragment} content
+ * @param {string|Element|DocumentFragment|DocumentOrShadowRoot} target
+ * @param {ObserverConfigObject} config
+ * @returns {Element|DocumentFragment}
+ */
+export function $render(content, target, { stack, signal, base = document } = {}) {
+ if (content instanceof HTMLTemplateElement) {
+ return $render(content.content.cloneNode(true), target, { stack, signal, base });
+ } else if (typeof target === 'string') {
+ return $render(content, base.getElementById(target), { stack, signal, base });
+ } else if (! (target instanceof Element || target instanceof DocumentFragment || target instanceof ShadowRoot)) {
+ throw new TypeError('Target must be an element or an ID.');
+ } else if (! (content instanceof Element || content instanceof DocumentFragment)) {
+ throw new TypeError('Content must be an Element or DocumentFragment.');
+ } else {
+ $observe(content, { stack, signal });
+ target.replaceChildren(content);
+ return content;
+ }
+}