From 941a140dca60aa98e7103fa7351cd9ba7a0014e4 Mon Sep 17 00:00:00 2001 From: Chris Zuber Date: Fri, 13 Mar 2026 12:17:12 -0700 Subject: [PATCH] Add support for boolean attribute signals Also adds `$render()` to observe content and replace children on target. --- CHANGELOG.md | 6 ++++ index.html | 1 + index.js | 42 ++++++++++++++++---------- iota.js | 2 +- package-lock.json | 4 +-- package.json | 2 +- watcher.js | 75 ++++++++++++++++++++++++++++++++++++++++++++--- 7 files changed, 108 insertions(+), 24 deletions(-) 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 @@

Hello, World!

+
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}!

+
- +
- - + +
-`); + + + +

Lorem Ipsum

+ +
+`, '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; + } +}