Añadir src/sigpro/sigpro.js
This commit is contained in:
171
src/sigpro/sigpro.js
Normal file
171
src/sigpro/sigpro.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* SigPro 2.0 - Atomic Unified Reactive Engine
|
||||
*/
|
||||
(() => {
|
||||
/** @type {Function|null} */
|
||||
let activeEffect = null;
|
||||
|
||||
/**
|
||||
* @typedef {Object} SigPro
|
||||
* @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element. Supports attributes, events, and reactive signals.
|
||||
* @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Clears the target and appends the root component to the DOM.
|
||||
* @property {function(Array): HTMLElement} router - Initializes a hash-based router that swaps components reactively.
|
||||
* @property {function(function, Object=): void} use - Extends SigPro functionality by injecting the $ object into a plugin function.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a Signal, Computed, or Effect.
|
||||
* - If a value is passed: Creates a Signal.
|
||||
* - If a function is passed: Creates a Computed/Effect that auto-tracks dependencies.
|
||||
* * @example
|
||||
* const $count = $(0); // Signal
|
||||
* const $double = $(() => $count() * 2); // Computed
|
||||
* * @param {any} initial - The initial value or a reactive function.
|
||||
* @returns {Function & SigPro} Reactive accessor/mutator with SigPro tools attached.
|
||||
*/
|
||||
const $ = (initial) => {
|
||||
const subs = new Set();
|
||||
const signal = (...args) => {
|
||||
if (args.length) {
|
||||
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
|
||||
if (!Object.is(initial, next)) {
|
||||
initial = next;
|
||||
subs.forEach(s => s());
|
||||
}
|
||||
}
|
||||
if (activeEffect) subs.add(activeEffect);
|
||||
return initial;
|
||||
};
|
||||
|
||||
if (typeof initial === 'function') {
|
||||
let cached;
|
||||
const runner = () => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = runner;
|
||||
try { cached = initial(); } finally { activeEffect = prev; }
|
||||
subs.forEach(s => s());
|
||||
};
|
||||
runner();
|
||||
return signal;
|
||||
}
|
||||
return signal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hyperscript engine to render HTML nodes.
|
||||
* Handles attributes (class, id), events (onclick), and reactive props ($value, $textContent).
|
||||
* * @param {string} tag - The HTML tag name (e.g., 'div', 'button').
|
||||
* @param {Object} [props] - Object containing attributes, events, or $reactive_props.
|
||||
* @param {any} [content] - String, Node, Array of nodes, or reactive function.
|
||||
* @returns {HTMLElement} A live DOM element.
|
||||
*/
|
||||
$.html = (tag, props = {}, content = []) => {
|
||||
const el = document.createElement(tag);
|
||||
if (typeof props !== 'object' || props instanceof Node || Array.isArray(props) || typeof props === 'function') {
|
||||
content = props;
|
||||
props = {};
|
||||
}
|
||||
|
||||
for (let [key, val] of Object.entries(props)) {
|
||||
if (key.startsWith('on')) {
|
||||
el.addEventListener(key.toLowerCase().slice(2), val);
|
||||
} else if (key.startsWith('$')) {
|
||||
const attr = key.slice(1);
|
||||
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
|
||||
const ev = attr === 'checked' ? 'change' : 'input';
|
||||
el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value));
|
||||
}
|
||||
$(() => {
|
||||
const v = typeof val === 'function' ? val() : val;
|
||||
if (attr === 'value' || attr === 'checked') el[attr] = v;
|
||||
else if (typeof v === 'boolean') el.toggleAttribute(attr, v);
|
||||
else el.setAttribute(attr, v ?? '');
|
||||
});
|
||||
} else el.setAttribute(key, val);
|
||||
}
|
||||
|
||||
const append = (c) => {
|
||||
if (Array.isArray(c)) return c.forEach(append);
|
||||
if (typeof c === 'function') {
|
||||
const node = document.createTextNode('');
|
||||
$(() => {
|
||||
const res = c();
|
||||
if (res instanceof Node) {
|
||||
if (node.parentNode) node.replaceWith(res);
|
||||
} else {
|
||||
node.textContent = res ?? '';
|
||||
}
|
||||
});
|
||||
return el.appendChild(node);
|
||||
}
|
||||
el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
|
||||
};
|
||||
append(content);
|
||||
return el;
|
||||
};
|
||||
|
||||
/**
|
||||
* Application mounter.
|
||||
* @param {HTMLElement|Function} node - The root component or element to mount.
|
||||
* @param {HTMLElement|string} [target] - The DOM element or selector where the app will be injected.
|
||||
*/
|
||||
$.mount = (node, target = document.body) => {
|
||||
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
||||
if (el) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(typeof node === 'function' ? node() : node);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reactive Client-side Hash Router.
|
||||
* Tracks window.location.hash and renders the matching component.
|
||||
* * @param {Array<{path: string, component: Function|HTMLElement}>} routes - Array of route definitions.
|
||||
* @returns {HTMLElement} A reactive container that updates on route change.
|
||||
*/
|
||||
$.router = (routes) => {
|
||||
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||
|
||||
return $.html('div', [
|
||||
() => {
|
||||
const current = sPath();
|
||||
const route = routes.find(r => {
|
||||
const rP = r.path.split('/').filter(Boolean);
|
||||
const cP = current.split('/').filter(Boolean);
|
||||
if (rP.length !== cP.length) return false;
|
||||
return rP.every((part, i) => part.startsWith(':') || part === cP[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
let component = route
|
||||
? (typeof route.component === 'function' ? route.component() : route.component)
|
||||
: $.html('h1', "404");
|
||||
|
||||
if (!component) return $.html('h1', "404");
|
||||
return component instanceof Node ? component : $.html('span', String(component));
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Programmatic navigation.
|
||||
* @param {string} path - The path to navigate to (e.g., '/home').
|
||||
*/
|
||||
$.router.go = (path) => {
|
||||
window.location.hash = path.startsWith('/') ? path : `/${path}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* SigPro Plugin System.
|
||||
* @param {function(SigPro, Object): void} plugin - Function that receives the SigPro instance.
|
||||
* @param {Object} [options] - Configuration for the plugin.
|
||||
*/
|
||||
$.use = (plugin, options = {}) => {
|
||||
if (typeof plugin === 'function') plugin($, options);
|
||||
};
|
||||
|
||||
const tags = ['div', 'span', 'p', 'button', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'label', 'section', 'nav', 'main', 'header', 'footer', 'input', 'form', 'img', 'select', 'option', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'canvas', 'video', 'audio'];
|
||||
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
|
||||
|
||||
window.$ = $;
|
||||
})();
|
||||
Reference in New Issue
Block a user