@zenithcore/core
Version:
Core functionality for ZenithKernel framework
757 lines (670 loc) • 30.4 kB
text/typescript
/**
* SignalDOMBinder - Advanced DOM binding utilities for reactive signals
* Extends DOM binding capabilities with performance optimizations and complex binding types
*/
import { Signal, isSignal, effect, computed, signal, Computation } from '../signals';
export interface DOMBindingOptions {
debounce?: number; // Debounce updates in milliseconds
immediate?: boolean; // Apply binding immediately (defaults to true)
priority?: 'low' | 'normal' | 'high'; // (Currently conceptual, not deeply implemented for scheduling)
transform?: (value: any) => any; // Value transformation
validator?: (value: any, oldValue?: any) => boolean; // Value validation
fallback?: any; // Fallback value on error or validation failure
}
export interface AnimationBindingOptions extends DOMBindingOptions {
duration?: number; // Animation duration in ms
easing?: string; // CSS easing function (e.g., 'linear', 'ease-in-out', 'cubic-bezier(...)')
keyframes?: Keyframe[] | PropertyIndexedKeyframes; // Custom keyframes
// fill?: AnimationFillMode; // if more control over Web Animations API is needed
}
export interface ConditionalBindingOptions extends DOMBindingOptions {
condition: Signal<boolean> | (() => boolean); // Condition signal or function
alternate?: any; // Value when condition is false
}
interface ComputationEffect {
dispose: () => void;
}
let bindingCounter = 0; // For generating unique binding IDs
/**
* Advanced DOM binding utilities with signal reactivity
*/
export class SignalDOMBinder {
private bindings = new Map<string, ComputationEffect>(); private debounceTimers = new Map<string, number>(); // bindingId -> timeoutId
private elementIdCache = new WeakMap<Node, string>(); // Element -> generated unique ID
private static nextElementId = 0;
/**
* Generic private helper to create and manage binding effects.
*/
private _createBindingEffect(
bindingId: string,
updateFn: () => void,
options: DOMBindingOptions
): Computation {
// Apply immediately if requested (default is true)
if (options.immediate !== false) {
updateFn();
}
const disposeEffect = effect(() => {
if (options.debounce && options.debounce > 0) {
this.debounceUpdate(bindingId, updateFn, options.debounce);
} else {
// Consider priority here if implementing advanced scheduling
updateFn();
}
});
this.bindings.set(bindingId, disposeEffect);
return disposeEffect;
}
/**
* Bind signal to element attribute with advanced options
*/
bindAttribute(
element: HTMLElement,
attribute: string,
signalValue: Signal<any> | any, // Can bind a signal or a static value (though less common for "bind")
options: DOMBindingOptions = {}
): string {
const bindingId = this.generateBindingId(element, 'attr', attribute);
const signal = isSignal(signalValue) ? signalValue : computed(() => signalValue);
const updateFn = () => {
try {
let value = signal.value;
const oldValue = element.getAttribute(attribute);
if (options.transform) value = options.transform(value);
if (options.validator && !options.validator(value, oldValue)) {
value = options.fallback;
}
const stringValue = value != null ? String(value) : null;
if (stringValue !== null) {
if (element.getAttribute(attribute) !== stringValue) {
element.setAttribute(attribute, stringValue);
}
} else {
if (element.hasAttribute(attribute)) {
element.removeAttribute(attribute);
}
}
} catch (error) {
console.error(`Error updating attribute ${attribute} for ${this.getElementId(element)}:`, error);
if (options.fallback !== undefined) {
if (element.getAttribute(attribute) !== String(options.fallback)) {
element.setAttribute(attribute, String(options.fallback));
}
}
}
};
this._createBindingEffect(bindingId, updateFn, options);
return bindingId;
}
/**
* Bind signal to element style with CSS property support
*/
bindStyle(
element: HTMLElement,
property: string, // Can be camelCase or kebab-case
signalValue: Signal<any> | any,
options: DOMBindingOptions = {}
): string {
const bindingId = this.generateBindingId(element, 'style', property);
const signal = isSignal(signalValue) ? signalValue : computed(() => signalValue);
const cssProperty = property.includes('-') ? property : property.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
const updateFn = () => {
try {
let value = signal.value;
const oldValue = element.style.getPropertyValue(cssProperty);
if (options.transform) value = options.transform(value);
if (options.validator && !options.validator(value, oldValue)) {
value = options.fallback;
}
const stringValue = value != null ? String(value) : '';
if (element.style.getPropertyValue(cssProperty) !== stringValue) {
if (stringValue) {
element.style.setProperty(cssProperty, stringValue);
} else {
element.style.removeProperty(cssProperty);
}
}
} catch (error) {
console.error(`Error updating style ${property} for ${this.getElementId(element)}:`, error);
// Fallback for styles might be complex if it needs to revert, often just log.
}
};
this._createBindingEffect(bindingId, updateFn, options);
return bindingId;
}
/**
* Bind signal to element class list with complex class logic
*/
bindClassList(
element: HTMLElement,
signalValue: Signal<string | string[] | Record<string, boolean>> | any,
options: DOMBindingOptions = {}
): string {
const bindingId = this.generateBindingId(element, 'class');
const signal = isSignal(signalValue) ? signalValue : computed(() => signalValue);
let previousClasses = new Set<string>();
const updateFn = () => {
try {
let value = signal.value;
if (options.transform) value = options.transform(value);
const newClasses = new Set<string>();
if (typeof value === 'string') {
value.split(/\s+/).filter(Boolean).forEach(cls => newClasses.add(cls));
} else if (Array.isArray(value)) {
(value as string[]).filter(Boolean).forEach(cls => newClasses.add(cls));
} else if (value && typeof value === 'object') {
for (const [className, condition] of Object.entries(value as Record<string, boolean>)) {
if (condition) newClasses.add(className);
}
}
// More efficient update:
previousClasses.forEach(cls => {
if (!newClasses.has(cls)) element.classList.remove(cls);
});
newClasses.forEach(cls => {
if (!previousClasses.has(cls)) element.classList.add(cls);
});
previousClasses = newClasses;
} catch (error) {
console.error(`Error updating class list for ${this.getElementId(element)}:`, error);
}
};
this._createBindingEffect(bindingId, updateFn, options);
return bindingId;
}
/**
* Bind signal to text content with formatting support
*/
bindTextContent(
node: Node, // HTMLElement or Text node
signalValue: Signal<any> | any,
options: DOMBindingOptions = {}
): string {
const bindingId = this.generateBindingId(node, 'text');
const signal = isSignal(signalValue) ? signalValue : computed(() => signalValue);
const updateFn = () => {
try {
let value = signal.value;
const oldValue = node.textContent;
if (options.transform) value = options.transform(value);
if (options.validator && !options.validator(value, oldValue)) {
value = options.fallback;
}
const stringValue = String(value ?? '');
if (node.textContent !== stringValue) {
node.textContent = stringValue;
}
} catch (error) {
console.error(`Error updating text content for ${this.getElementId(node)}:`, error);
if (options.fallback !== undefined) {
if (node.textContent !== String(options.fallback)) {
node.textContent = String(options.fallback);
}
}
}
};
this._createBindingEffect(bindingId, updateFn, options);
return bindingId;
}
/**
* Animated binding with CSS transitions or Web Animations API
*/
bindAnimated(
element: HTMLElement,
property: string, // CSS property to animate (e.g., 'opacity', 'transform')
signalValue: Signal<any> | any,
options: AnimationBindingOptions = {}
): string {
const bindingId = this.generateBindingId(element, 'animated', property);
const signal = isSignal(signalValue) ? signalValue : computed(() => signalValue);
const { duration = 300, easing = 'ease', keyframes, ...baseOptions } = options;
const updateFn = () => {
try {
let value = signal.value;
if (baseOptions.transform) value = baseOptions.transform(value);
// Validator could also be used here if needed
if (keyframes) {
// This assumes 'value' might influence the keyframes or be one of them.
// More typically, keyframes are predefined and the signal triggers the animation.
// If signal.value IS the target state for a simpler property animation:
const dynamicKeyframes = Array.isArray(keyframes)
? keyframes // Use as is
: { ...keyframes, [property]: value } as PropertyIndexedKeyframes; // Or merge if value is target
element.animate(dynamicKeyframes, { duration, easing, fill: 'forwards' });
} else {
const cssProperty = property.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
if (!element.style.transition) { // Avoid overwriting existing complex transitions unintentionally
element.style.transition = `${cssProperty} ${duration}ms ${easing}`;
}
// Check current value before setting to avoid re-triggering if value is same
if (element.style.getPropertyValue(cssProperty) !== String(value)) {
element.style.setProperty(cssProperty, String(value));
}
}
} catch (error) {
console.error(`Error animating property ${property} for ${this.getElementId(element)}:`, error);
}
};
this._createBindingEffect(bindingId, updateFn, baseOptions);
return bindingId;
}
/**
* Conditional binding that only updates when condition is met
*/
bindConditional(
element: HTMLElement,
attribute: string,
signalValue: Signal<any> | any, // Value to set when condition is true
options: ConditionalBindingOptions
): string {
const bindingId = this.generateBindingId(element, 'conditional', attribute);
const dataSignal = isSignal(signalValue) ? signalValue : computed(() => signalValue);
const conditionSignal = typeof options.condition === 'function' ? computed(options.condition) : options.condition;
const updateFn = () => {
try {
if (conditionSignal.value) {
let value = dataSignal.value;
if (options.transform) value = options.transform(value);
// Validator could be added here too
if (element.getAttribute(attribute) !== String(value)) {
element.setAttribute(attribute, String(value));
}
} else if (options.alternate !== undefined) {
if (element.getAttribute(attribute) !== String(options.alternate)) {
element.setAttribute(attribute, String(options.alternate));
}
} else {
if (element.hasAttribute(attribute)) {
element.removeAttribute(attribute);
}
}
} catch (error) {
console.error(`Error in conditional binding for ${attribute} on ${this.getElementId(element)}:`, error);
}
};
// Create effect based on both dataSignal and conditionSignal
const disposeEffect = effect(() => {
// Access both signals to ensure effect re-runs when either changes
dataSignal.value;
conditionSignal.value;
if (options.debounce && options.debounce > 0) {
this.debounceUpdate(bindingId, updateFn, options.debounce);
} else {
updateFn();
}
});
this.bindings.set(bindingId, disposeEffect);
if (options.immediate !== false) updateFn(); // Initial sync
return bindingId;
}
/**
* Bind to element visibility with intersection observer
*/
bindVisibility(
element: HTMLElement,
visibilitySignal: Signal<boolean>, // This signal will be UPDATED by the observer
options: Pick<DOMBindingOptions, 'priority'> & { threshold?: number | number[] } = {}
): string {
const bindingId = this.generateBindingId(element, 'visibility');
if (!isSignal(visibilitySignal)) {
console.warn(`bindVisibility expects a writable Signal to update. Provided value is not a Signal for element ${this.getElementId(element)}.`);
return bindingId; // Or throw error
}
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (visibilitySignal.value !== entry.isIntersecting) {
visibilitySignal.value = entry.isIntersecting;
}
},
{ threshold: options.threshold || 0.1 } // Default to 10% visibility
);
observer.observe(element);
const dispose = () => observer.disconnect();
this.bindings.set(bindingId, { dispose } as ComputationEffect);
return bindingId;
}
/**
* Bind form input value with two-way data binding
*/
bindInputValue(
input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
valueSignal: Signal<string | number | string[]>, // string[] for select multiple
options: DOMBindingOptions = {}
): string {
const bindingId = this.generateBindingId(input, 'input-value');
const updateInputFromSignal = () => {
const signalVal = valueSignal.value;
if (input.type === 'checkbox' && typeof signalVal === 'boolean') { // Should use bindCheckbox
(input as HTMLInputElement).checked = signalVal;
} else if (input.type === 'radio') {
(input as HTMLInputElement).checked = (input.value === String(signalVal));
} else if (input.tagName === 'SELECT' && (input as HTMLSelectElement).multiple && Array.isArray(signalVal)) {
const selectedValues = new Set(signalVal as string[]);
Array.from((input as HTMLSelectElement).options).forEach(opt => {
opt.selected = selectedValues.has(opt.value);
});
} else if (input.value !== String(signalVal ?? '')) {
input.value = String(signalVal ?? '');
}
};
// Effect to update input when signal changes
const disposeSignalEffect = effect(updateInputFromSignal);
if (options.immediate !== false) updateInputFromSignal();
const eventType = (input.type === 'checkbox' || input.type === 'radio' || input.tagName === 'SELECT') ? 'change' : 'input';
const inputUpdateHandler = (event: Event) => {
const target = event.target as typeof input;
let newValue: string | number | boolean | string[];
if (target.type === 'checkbox') {
newValue = (target as HTMLInputElement).checked;
} else if (target.type === 'number') {
newValue = (target as HTMLInputElement).valueAsNumber;
if (isNaN(newValue)) newValue = (target as HTMLInputElement).value; // fallback if not a valid number
} else if (target.tagName === 'SELECT' && (target as HTMLSelectElement).multiple) {
newValue = Array.from((target as HTMLSelectElement).selectedOptions).map(opt => opt.value);
}
else {
newValue = target.value;
}
const oldValue = valueSignal.value;
if (options.validator && !options.validator(newValue, oldValue)) {
updateInputFromSignal(); // Revert to signal value
return;
}
// Type casting might be needed if signal has a stricter type than string/number
if (valueSignal.value !== newValue) {
(valueSignal as Signal<any>).value = newValue;
}
};
input.addEventListener(eventType, inputUpdateHandler);
const dispose = () => {
disposeSignalEffect.dispose(); // Dispose the effect
input.removeEventListener(eventType, inputUpdateHandler);
};
this.bindings.set(bindingId, { dispose });
return bindingId;
}
// bindCheckbox is largely covered by bindInputValue with type='checkbox', but can be kept for explicitness
bindCheckbox(
checkbox: HTMLInputElement,
checkedSignal: Signal<boolean>,
options: DOMBindingOptions = {}
): string {
if (checkbox.type !== 'checkbox') {
console.warn("bindCheckbox should be used with input type='checkbox'.");
}
// Re-use bindInputValue, it handles checkboxes correctly if signal is boolean
return this.bindInputValue(checkbox, checkedSignal as Signal<any>, options);
}
/**
* Create a reactive list binding for dynamic element lists (Optimized)
*/
// In SignalDOMBinder class
bindList<T>(
container: HTMLElement,
itemsSignal: Signal<T[]>,
renderItem: (item: T, index: number, itemConcreteSignal: Signal<T>) => HTMLElement, // itemConcreteSignal is guaranteed
options: DOMBindingOptions & {
keyFn: (item: T, index: number) => string | number;
updateItem?: (element: HTMLElement, itemConcreteSignal: Signal<T>, index: number) => void; // itemConcreteSignal is guaranteed
itemSignalCreator?: (initialValue: T) => Signal<T>;
}
): string {
if (!options.keyFn) {
console.error("bindList requires a 'keyFn' option for efficient updates.");
return this.generateBindingId(container, 'list-error');
}
const bindingId = this.generateBindingId(container, 'list');
const elementMap = new Map<string | number, HTMLElement>();
const itemSignalMap = new Map<string | number, Signal<T>>();
let previousKeys: Array<string | number> = [];
const updateList = () => {
try {
const items = itemsSignal.value || [];
const newKeys: Array<string | number> = items.map(options.keyFn);
const newElementMap = new Map<string | number, HTMLElement>();
const newItemSignalMap = new Map<string | number, Signal<T>>();
const fragment = document.createDocumentFragment();
items.forEach((itemData, index) => {
const key = newKeys[index];
const existingElement = elementMap.get(key);
let itemConcreteSignal = itemSignalMap.get(key); // This can be Signal<T> | undefined
if (itemConcreteSignal) { // Existing item signal
if (itemConcreteSignal.value !== itemData) itemConcreteSignal.value = itemData;
} else { // New item signal
// Use the imported 'signal' function (lowercase)
itemConcreteSignal = options.itemSignalCreator ? options.itemSignalCreator(itemData) : signal(itemData);
}
newItemSignalMap.set(key, itemConcreteSignal); // itemConcreteSignal is now definitely Signal<T>
let currentElement: HTMLElement;
if (existingElement) { // Existing element
currentElement = existingElement;
if (options.updateItem) {
// Pass the now-guaranteed Signal<T>
options.updateItem(currentElement, itemConcreteSignal, index);
}
} else { // New element
// Pass the now-guaranteed Signal<T>
currentElement = renderItem(itemData, index, itemConcreteSignal);
}
newElementMap.set(key, currentElement);
fragment.appendChild(currentElement);
});
previousKeys.forEach(key => {
if (!newKeys.includes(key)) {
const elementToRemove = elementMap.get(key);
elementToRemove?.remove();
elementMap.delete(key);
itemSignalMap.delete(key);
}
});
container.innerHTML = '';
container.appendChild(fragment);
elementMap.clear(); newElementMap.forEach((el, k) => elementMap.set(k, el));
itemSignalMap.clear(); newItemSignalMap.forEach((sig, k) => itemSignalMap.set(k, sig));
previousKeys = newKeys;
} catch (error) {
console.error(`Error updating list binding for ${this.getElementId(container)}:`, error);
}
};
// The _createBindingEffect method will handle storing the ComputationEffect properly
this._createBindingEffect(bindingId, updateList, options);
return bindingId;
}
/**
* Remove a specific binding
*/
removeBinding(bindingId: string): void {
const computationEffect = this.bindings.get(bindingId); // Renamed for clarity
if (computationEffect) {
computationEffect.dispose(); // Call the .dispose() method
this.bindings.delete(bindingId);
const timer = this.debounceTimers.get(bindingId);
if (timer) {
clearTimeout(timer);
this.debounceTimers.delete(bindingId);
}
// Removed animationFrames cleanup as it wasn't being used
}
}
/**
* Remove all bindings for a specific element/node
*/
removeNodeBindings(node: Node): void {
const elementIdPrefix = this.getElementId(node) + '-'; // Used as a prefix
const bindingsToRemove: string[] = [];
for (const bindingId of this.bindings.keys()) {
// Check if the bindingId starts with the element's unique prefix part
// This relies on generateBindingId's structure.
if (bindingId.startsWith(elementIdPrefix)) {
bindingsToRemove.push(bindingId);
}
}
bindingsToRemove.forEach(id => this.removeBinding(id));
}
/**
* Clean up all bindings
*/
dispose(): void {
this.bindings.forEach(computationEffect => computationEffect.dispose()); // Call .dispose()
this.bindings.clear();
this.debounceTimers.forEach(timer => clearTimeout(timer));
this.debounceTimers.clear();
this.elementIdCache = new WeakMap();
}
/** Get binding statistics */
getStats() {
return {
totalBindings: this.bindings.size,
pendingDebounces: this.debounceTimers.size,
};
}
private generateBindingId(node: Node, type: string, key?: string): string {
const elementPart = this.getElementId(node);
const keyPart = key ? `-${key.replace(/[^a-zA-Z0-9_-]/g, '')}` : ''; // Sanitize key
return `${elementPart}-${type}${keyPart}-${bindingCounter++}`;
}
private getElementId(node: Node): string {
if (node instanceof HTMLElement && node.id) {
return node.id;
}
let generatedId = this.elementIdCache.get(node);
if (!generatedId) {
generatedId = `zenith-node-${SignalDOMBinder.nextElementId++}`;
this.elementIdCache.set(node, generatedId);
}
return generatedId;
}
private debounceUpdate(bindingId: string, updateFn: () => void, delay: number): void {
const existingTimer = this.debounceTimers.get(bindingId);
if (existingTimer) clearTimeout(existingTimer);
const timer = setTimeout(() => {
updateFn();
this.debounceTimers.delete(bindingId);
}, delay) as any as number; // Cast for Node.js/browser timer ID compatibility
this.debounceTimers.set(bindingId, timer);
}
}
let globalSignalDOMBinder: SignalDOMBinder | undefined;
export function getSignalDOMBinder(): SignalDOMBinder {
if (!globalSignalDOMBinder) {
globalSignalDOMBinder = new SignalDOMBinder();
}
return globalSignalDOMBinder;
}
export const domBindings = {
text: (node: Node, signal: Signal<any> | any, options?: DOMBindingOptions) =>
getSignalDOMBinder().bindTextContent(node, signal, options),
attr: (el: HTMLElement, attr: string, signal: Signal<any> | any, options?: DOMBindingOptions) =>
getSignalDOMBinder().bindAttribute(el, attr, signal, options),
style: (el: HTMLElement, prop: string, signal: Signal<any> | any, options?: DOMBindingOptions) =>
getSignalDOMBinder().bindStyle(el, prop, signal, options),
class: (el: HTMLElement, signal: Signal<any> | any, options?: DOMBindingOptions) =>
getSignalDOMBinder().bindClassList(el, signal, options),
input: (inputEl: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, signal: Signal<any>, options?: DOMBindingOptions) =>
getSignalDOMBinder().bindInputValue(inputEl, signal, options),
checkbox: (checkboxEl: HTMLInputElement, signal: Signal<boolean>, options?: DOMBindingOptions) =>
getSignalDOMBinder().bindCheckbox(checkboxEl, signal, options),
animate: (el: HTMLElement, prop: string, signal: Signal<any> | any, options?: AnimationBindingOptions) =>
getSignalDOMBinder().bindAnimated(el, prop, signal, options),
list: <T>(
container: HTMLElement,
itemsSignal: Signal<T[]>,
renderItem: (item: T, index: number, itemSignal: Signal<T>) => HTMLElement,
options: DOMBindingOptions & {
keyFn: (item: T, index: number) => string | number;
updateItem?: (element: HTMLElement, itemSignal: Signal<T>, index: number) => void;
itemSignalCreator?: (initialValue: T) => Signal<T>;
}
) => getSignalDOMBinder().bindList(container, itemsSignal, renderItem, options),
visible: (el: HTMLElement, visibilitySignal: Signal<boolean>, options?: Pick<DOMBindingOptions, 'priority'> & { threshold?: number | number[] }) =>
getSignalDOMBinder().bindVisibility(el, visibilitySignal, options),
conditional: (el: HTMLElement, attribute: string, signalValue: Signal<any> | any, options: ConditionalBindingOptions) =>
getSignalDOMBinder().bindConditional(el, attribute, signalValue, options),
remove: (bindingId: string) => getSignalDOMBinder().removeBinding(bindingId),
removeAll: (node: Node) => getSignalDOMBinder().removeNodeBindings(node),
};
export function createReactiveElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
staticProps: Partial<Omit<HTMLElementTagNameMap[K], 'style' | 'classList' | 'dataset' | 'textContent' | 'innerHTML'>> & {
style?: Partial<CSSStyleDeclaration> | Record<string, string | number>;
className?: string; // For static classes
class?: string; // Alias for className
textContent?: string;
innerHTML?: string;
ref?: (el: HTMLElementTagNameMap[K]) => void;
children?: Array<Node | string>;
} = {},
reactiveBindings?: { // More specific reactive bindings
text?: Signal<any> | any;
html?: Signal<string> | string; // For binding innerHTML reactively (use with caution)
visible?: Signal<boolean>; // Bind visibility
class?: Signal<string | string[] | Record<string, boolean>> | any;
attrs?: Record<string, Signal<any> | any>;
styles?: Record<string, Signal<any> | any>; // PropertyName: SignalOrValue
events?: Record<string, (event: Event) => void>; // Static event listeners
},
optionsStore: { // Global options for these bindings
attrs?: DOMBindingOptions;
styles?: DOMBindingOptions;
text?: DOMBindingOptions;
class?: DOMBindingOptions;
} = {}
): HTMLElementTagNameMap[K] {
const element = document.createElement(tagName);
const binder = getSignalDOMBinder();
for (const [key, value] of Object.entries(staticProps)) {
if (key.startsWith('on') && typeof value === 'function') {
element.addEventListener(key.slice(2).toLowerCase(), value as EventListener);
} else if (key === 'className' || key === 'class') {
element.className = String(value);
} else if (key === 'textContent') {
element.textContent = String(value);
} else if (key === 'innerHTML' && typeof value === 'string') { // DANGEROUSLY_SET_INNER_HTML
element.innerHTML = value;
} else if (key === 'style' && typeof value === 'object' && value !== null) {
Object.entries(value as Record<string, string | number>).forEach(([styleProp, styleVal]) => {
(element.style as any)[styleProp.includes('-') ? styleProp.replace(/-(\w)/g, (_, c) => c.toUpperCase()) : styleProp] = styleVal;
});
} else if (key === 'ref' && typeof value === 'function') {
(value as (el: HTMLElementTagNameMap[K]) => void)(element);
} else if (key === 'children' && Array.isArray(value)) {
(value as Array<Node | string>).forEach(child => {
element.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
});
}
else {
element.setAttribute(key, String(value));
}
}
if (reactiveBindings) {
if (reactiveBindings.text !== undefined) binder.bindTextContent(element, reactiveBindings.text, optionsStore.text);
if (reactiveBindings.html !== undefined) { // DANGEROUSLY_BIND_INNER_HTML
const htmlSignal = isSignal(reactiveBindings.html) ? reactiveBindings.html : computed(() => reactiveBindings.html);
effect(() => { element.innerHTML = String(htmlSignal.value ?? ''); }); // Simple effect, could be expanded
}
if (reactiveBindings.visible !== undefined) binder.bindVisibility(element, reactiveBindings.visible);
if (reactiveBindings.class !== undefined) binder.bindClassList(element, reactiveBindings.class, optionsStore.class);
if (reactiveBindings.attrs) {
Object.entries(reactiveBindings.attrs).forEach(([attrName, signalValue]) => {
binder.bindAttribute(element, attrName, signalValue, optionsStore.attrs);
});
}
if (reactiveBindings.styles) {
Object.entries(reactiveBindings.styles).forEach(([styleProp, signalValue]) => {
binder.bindStyle(element, styleProp, signalValue, optionsStore.styles);
});
}
if (reactiveBindings.events) { // Static event listeners from reactiveBindings, same as staticProps.on...
Object.entries(reactiveBindings.events).forEach(([eventName, handler]) => {
element.addEventListener(eventName.toLowerCase(), handler);
});
}
}
return element;
}
export function createReactiveText(textSignal: Signal<string | number | null | undefined>, options?: DOMBindingOptions): Text {
const textNode = document.createTextNode('');
getSignalDOMBinder().bindTextContent(textNode, textSignal, options);
return textNode;
}