Mite/src/Mite.js

252 lines
10 KiB
JavaScript
Raw Normal View History

2025-03-27 18:59:19 +00:00
/**
* Signal
*
* Creates a signal for reactive state management
*
* @param {any} value - Initial value for the signal
*
* @returns {Object} Signal object with getter, setter, and subscription methods
*/
2025-03-27 17:16:11 +00:00
function signal(value) {
const subscribers = new Set();
return {
2025-03-27 18:59:19 +00:00
// gets current signal value
2025-03-27 17:16:11 +00:00
get value() {
return value;
},
2025-03-27 18:59:19 +00:00
// sets a new value for the signal, notifying all subscribers
2025-03-27 17:16:11 +00:00
set value(newValue) {
value = newValue;
subscribers.forEach(fn => fn());
},
2025-03-27 18:59:19 +00:00
// subscribe to value changes
2025-03-27 17:16:11 +00:00
subscribe(fn) {
subscribers.add(fn);
return () => subscribers.delete(fn);
}
};
}
export class Mite {
constructor(selector, options) {
this.root = document.querySelector(selector);
if (!this.root) throw new Error(`Element ${selector} not found`);
2025-03-29 14:49:45 +00:00
this.data = {};
for (const key in options.data) {
this.data[key] = signal(options.data[key]);
2025-03-27 17:16:11 +00:00
}
this.methods = options || {};
this.lifecycle = {
mounted: options.mounted || (() => {}),
unmounted: options.unmounted || (() => {})
};
2025-03-27 17:27:49 +00:00
for (const key in this.methods) {
if (typeof this.methods[key] === "function") {
2025-03-27 17:16:11 +00:00
this.methods[key] = this.methods[key].bind(this);
}
}
this.template = this.root.cloneNode(true);
this.bindings = new Map();
this.render();
this.lifecycle.mounted.call(this);
}
render() {
this.bindings.forEach(binding => binding.forEach(b => b.unsubscribe?.()));
this.bindings.clear();
const fragment = this.template.cloneNode(true);
2025-03-27 17:29:45 +00:00
this.root.innerHTML = "";
2025-03-27 17:16:11 +00:00
this.root.appendChild(fragment);
this.scanAndBind(this.root);
2025-03-29 14:49:45 +00:00
for (const key in this.data) {
2025-03-27 17:16:11 +00:00
this.update(key);
}
}
scanAndBind(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
2025-03-27 17:29:45 +00:00
if (node.nodeType === Node.TEXT_NODE && node.textContent.includes("{")) {
2025-03-27 17:16:11 +00:00
const matches = node.textContent.match(/{(\w+)}/g);
if (matches) {
matches.forEach(token => {
const key = token.slice(1, -1);
2025-03-29 14:49:45 +00:00
if (this.data[key]) {
2025-03-27 17:16:11 +00:00
if (!this.bindings.has(key)) this.bindings.set(key, []);
const updateFn = () => this.update(key);
2025-03-29 14:49:45 +00:00
const unsubscribe = this.data[key].subscribe(updateFn);
2025-03-27 17:29:45 +00:00
this.bindings.get(key).push({ node, type: "text", original: node.textContent, unsubscribe });
2025-03-27 17:16:11 +00:00
}
});
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
2025-03-27 17:27:49 +00:00
for (const attr of node.attributes) {
2025-03-27 17:16:11 +00:00
const matches = attr.value.match(/{(\w+)}/g);
if (matches) {
2025-03-27 17:29:45 +00:00
if (attr.name.startsWith("@")) {
2025-03-27 17:16:11 +00:00
const eventName = attr.name.slice(1);
const methodName = attr.value.slice(1, -1);
if (this.methods[methodName]) {
node.addEventListener(eventName, this.methods[methodName]);
}
} else {
matches.forEach(token => {
const key = token.slice(1, -1);
2025-03-29 14:49:45 +00:00
if (this.data[key]) {
2025-03-27 17:16:11 +00:00
if (!this.bindings.has(key)) this.bindings.set(key, []);
const updateFn = () => this.update(key);
2025-03-29 14:49:45 +00:00
const unsubscribe = this.data[key].subscribe(updateFn);
2025-03-27 17:29:45 +00:00
this.bindings.get(key).push({ node, type: "attr", attrName: attr.name, original: attr.value, unsubscribe });
2025-03-27 17:16:11 +00:00
}
});
}
}
}
2025-03-27 17:29:45 +00:00
const rif = node.getAttribute("r-if");
2025-03-27 17:16:11 +00:00
if (rif && rif.match(/{(\w+)}$/)) {
const key = rif.slice(1, -1);
2025-03-29 14:49:45 +00:00
if (this.data[key]) {
2025-03-27 17:16:11 +00:00
if (!this.bindings.has(key)) this.bindings.set(key, []);
const updateFn = () => this.update(key);
2025-03-29 14:49:45 +00:00
const unsubscribe = this.data[key].subscribe(updateFn);
2025-03-27 17:29:45 +00:00
this.bindings.get(key).push({ node, type: "if", unsubscribe });
2025-03-27 17:16:11 +00:00
}
}
2025-03-27 17:29:45 +00:00
const reach = node.getAttribute("r-each");
2025-03-27 17:16:11 +00:00
if (reach) {
const match = reach.match(/{(.+?)}/);
if (!match) {
console.error(`Invalid r-each syntax: ${reach}`);
continue;
}
const content = match[1];
2025-03-27 17:29:45 +00:00
const parts = content.split(" in ");
2025-03-27 17:16:11 +00:00
if (parts.length !== 2) {
console.error(`Invalid r-each split on " in ": ${content}, parts: ${parts}`);
continue;
}
let [names, listName] = parts;
names = names.trim();
listName = listName.trim();
2025-03-27 17:29:45 +00:00
const nameParts = names.split(",").map(part => part.trim());
2025-03-27 17:16:11 +00:00
const itemName = nameParts[0];
const indexName = nameParts.length > 1 ? nameParts[1] : undefined;
2025-03-29 14:49:45 +00:00
if (this.data[listName]) {
2025-03-27 17:16:11 +00:00
if (!this.bindings.has(listName)) this.bindings.set(listName, []);
const updateFn = () => this.update(listName);
2025-03-29 14:49:45 +00:00
const unsubscribe = this.data[listName].subscribe(updateFn);
2025-03-27 17:16:11 +00:00
this.bindings.get(listName).push({
node,
2025-03-27 17:29:45 +00:00
type: "each",
2025-03-27 17:16:11 +00:00
itemName,
indexName,
unsubscribe,
originalReach: reach,
parentSelector: node.parentNode.tagName.toLowerCase()
});
} else {
2025-03-29 14:49:45 +00:00
console.error(`data key not found: ${listName}`);
2025-03-27 17:16:11 +00:00
}
}
}
}
}
update(key) {
const bindings = this.bindings.get(key) || [];
bindings.forEach(binding => {
2025-03-27 17:29:45 +00:00
// Remove the parentNode check for "each" bindings since we use parentSelector
2025-03-27 17:16:11 +00:00
if (!binding.node) return;
switch (binding.type) {
2025-03-27 17:29:45 +00:00
case "text":
2025-03-27 17:16:11 +00:00
if (!binding.node.parentNode) return;
binding.node.textContent = binding.original.replace(/{(\w+)}/g, (_, k) => {
2025-03-29 14:49:45 +00:00
return this.data[k] ? this.data[k].value : `{${k}}`;
2025-03-27 17:16:11 +00:00
});
break;
2025-03-27 17:29:45 +00:00
case "attr":
2025-03-27 17:16:11 +00:00
if (!binding.node.parentNode) return;
binding.node.setAttribute(binding.attrName, binding.original.replace(/{(\w+)}/g, (_, k) => {
2025-03-29 14:49:45 +00:00
return this.data[k] ? this.data[k].value : `{${k}}`;
2025-03-27 17:16:11 +00:00
}));
break;
2025-03-27 17:29:45 +00:00
case "if":
2025-03-27 17:16:11 +00:00
if (!binding.node.parentNode) return;
2025-03-29 14:49:45 +00:00
binding.node.style.display = this.data[key].value ? "" : "none";
2025-03-27 17:16:11 +00:00
break;
2025-03-27 17:29:45 +00:00
case "each": {
2025-03-27 17:27:49 +00:00
const parent = this.root.querySelector(binding.parentSelector);
2025-03-27 17:16:11 +00:00
if (!parent) {
console.error(`Parent node (${binding.parentSelector}) not found for r-each="${binding.originalReach}"`);
return;
}
const template = this.template.querySelector(`[r-each="${binding.originalReach}"]`);
if (!template) {
console.error(`Template not found for r-each="${binding.originalReach}"`);
return;
}
2025-03-29 14:49:45 +00:00
const items = this.data[key].value || [];
2025-03-27 17:16:11 +00:00
2025-03-27 17:29:45 +00:00
parent.innerHTML = "";
2025-03-27 17:16:11 +00:00
items.forEach((item, index) => {
const clone = template.cloneNode(true);
2025-03-27 17:29:45 +00:00
clone.removeAttribute("r-each");
2025-03-27 17:16:11 +00:00
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
let textNode;
while ((textNode = walker.nextNode())) {
2025-03-27 17:29:45 +00:00
if (textNode.textContent.includes("{")) {
2025-03-27 17:16:11 +00:00
textNode.textContent = textNode.textContent.replace(/{(\w+)}/g, (_, k) => {
if (k === binding.itemName) return item;
if (k === binding.indexName) return index;
2025-03-29 14:49:45 +00:00
return this.data[k] ? this.data[k].value : `{${k}}`;
2025-03-27 17:16:11 +00:00
});
}
}
2025-03-27 17:29:45 +00:00
const eventElements = clone.querySelectorAll("[\\@click]");
2025-03-27 17:16:11 +00:00
eventElements.forEach(el => {
2025-03-27 17:29:45 +00:00
const clickAttr = el.getAttribute("@click");
2025-03-27 17:16:11 +00:00
if (clickAttr && clickAttr.match(/{(\w+),\s*(\w+)}/)) {
const [, methodName, param] = clickAttr.match(/{(\w+),\s*(\w+)}/);
if (this.methods[methodName] && param === binding.indexName) {
2025-03-27 17:29:45 +00:00
el.addEventListener("click", (e) => this.methods[methodName](e, index));
2025-03-27 17:16:11 +00:00
}
}
});
parent.appendChild(clone);
});
break;
2025-03-27 17:27:49 +00:00
}
2025-03-27 17:16:11 +00:00
}
});
}
destroy() {
this.lifecycle.unmounted.call(this);
this.bindings.forEach(binding => binding.forEach(b => b.unsubscribe?.()));
this.root.innerHTML = this.template.innerHTML;
}
}
export default Mite;