- fixed two way binding
- added support for list like attributes to toggle classes for example
This commit is contained in:
parent
0f2874e6df
commit
870630063a
@ -16,7 +16,7 @@ export class Route {
|
||||
constructor(pattern, componentClass) {
|
||||
this.id = crypto.randomUUID();
|
||||
this.componentClass = componentClass;
|
||||
this.ComponentDefinition = componentClass.definition;
|
||||
this.componentDefinition = componentClass.definition;
|
||||
this.fragments = pattern.split("/");
|
||||
}
|
||||
|
||||
@ -27,9 +27,7 @@ export class Route {
|
||||
params[key] = value;
|
||||
});
|
||||
|
||||
const pathFragments = parsedUrl.pathname
|
||||
.split("/")
|
||||
.filter((f) => f.length > 0);
|
||||
const pathFragments = parsedUrl.pathname.split("/");
|
||||
|
||||
for (let i = 0; i < this.fragments.length; i++) {
|
||||
const fragment = this.fragments[i];
|
||||
@ -144,12 +142,14 @@ export class ComponentDefinition {
|
||||
templatesPath: null,
|
||||
template: null,
|
||||
stylesPath: null,
|
||||
style: null,
|
||||
scriptsPath: null,
|
||||
},
|
||||
) {
|
||||
this.templatesPath = options.templatesPath;
|
||||
this.template = options.template;
|
||||
this.stylesPath = options.stylesPath;
|
||||
this.style = options.style;
|
||||
this.scriptsPath = options.scriptsPath;
|
||||
}
|
||||
}
|
||||
@ -257,6 +257,7 @@ export class Router {
|
||||
}
|
||||
|
||||
async handleNavigate(event) {
|
||||
if (event.navigationType === 'replace' && event.destination.url === this.activeRoute) return;
|
||||
const routeMatch = this.findMatchingRoute(event.destination.url);
|
||||
if (!routeMatch) return;
|
||||
|
||||
@ -270,6 +271,7 @@ export class Router {
|
||||
this.destroyCurrent();
|
||||
this.activeController = controller;
|
||||
this.activeTemplate = template;
|
||||
this.activeRoute = event.destination.url;
|
||||
this.insertIncludes(routeMatch.route);
|
||||
|
||||
this._boundUpdateHandler = this.handleUpdateRequest.bind(this);
|
||||
@ -279,7 +281,7 @@ export class Router {
|
||||
);
|
||||
|
||||
this.renderActiveComponent();
|
||||
history.pushState(null, "", routeMatch.path);
|
||||
history.replaceState({}, "", event.destination.url);
|
||||
}
|
||||
|
||||
destroyCurrent() {
|
||||
@ -308,9 +310,17 @@ export class Router {
|
||||
insertIncludes(route) {
|
||||
// Remove previous component styles from shadow root
|
||||
this.shadowRoot
|
||||
.querySelectorAll("link[data-component-style]")
|
||||
.querySelectorAll("link[data-component-style], style[data-component-style]")
|
||||
.forEach((l) => l.remove());
|
||||
const paths = route.ComponentDefinition.stylesPath;
|
||||
|
||||
if (route.componentDefinition.style) {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.textContent = route.componentDefinition.style;
|
||||
styleEl.setAttribute("data-component-style", "");
|
||||
this.shadowRoot.appendChild(styleEl);
|
||||
}
|
||||
|
||||
const paths = route.componentDefinition.stylesPath;
|
||||
if (!paths) return;
|
||||
const list = Array.isArray(paths) ? paths : [paths];
|
||||
for (const p of list) {
|
||||
@ -400,20 +410,20 @@ export class Router {
|
||||
const allElements = root.querySelectorAll("*");
|
||||
|
||||
allElements.forEach((element) => {
|
||||
// ─── Two-way bindings from __templateMeta ───
|
||||
if (element.__templateMeta) {
|
||||
for (const meta of element.__templateMeta) {
|
||||
if (meta.bracket === "[()]" && meta.expression) {
|
||||
const domProp = meta.name.replace(/^\(|\)$/g, ""); // strip parens
|
||||
this._bindTwoWay(controller, element, domProp, meta.expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Event bindings from DOM attributes ───
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
const name = attr.name;
|
||||
|
||||
// ─── Two-way binding: [(prop)]="field" ───
|
||||
const twoWayMatch = name.match(/^\[\((\w+)\)\]$/);
|
||||
if (twoWayMatch) {
|
||||
const domProp = twoWayMatch[1]; // e.g. "value"
|
||||
const field = attr.value.trim(); // e.g. "name" (controller property)
|
||||
|
||||
this._bindTwoWay(controller, element, domProp, field);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ─── Event binding: (event)="handler()" ───
|
||||
const eventMatch = name.match(/^\((\w+)\)$/);
|
||||
if (eventMatch) {
|
||||
const eventName = eventMatch[1];
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import "./linq.js";
|
||||
import { formatDate } from "./utils.js";
|
||||
|
||||
// ─── Expression evaluation ───
|
||||
|
||||
@ -87,33 +88,7 @@ function formatNumber(value, spec) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatDate(date, spec) {
|
||||
const pad = (n, l = 2) => String(n).padStart(l, "0");
|
||||
const tokens = {
|
||||
yyyy: date.getFullYear(),
|
||||
yy: String(date.getFullYear()).slice(-2),
|
||||
MM: pad(date.getMonth() + 1),
|
||||
M: date.getMonth() + 1,
|
||||
dd: pad(date.getDate()),
|
||||
d: date.getDate(),
|
||||
HH: pad(date.getHours()),
|
||||
H: date.getHours(),
|
||||
hh: pad(date.getHours() % 12 || 12),
|
||||
h: date.getHours() % 12 || 12,
|
||||
mm: pad(date.getMinutes()),
|
||||
m: date.getMinutes(),
|
||||
ss: pad(date.getSeconds()),
|
||||
s: date.getSeconds(),
|
||||
fff: pad(date.getMilliseconds(), 3),
|
||||
};
|
||||
let result = spec;
|
||||
for (const [tok, val] of Object.entries(tokens).sort(
|
||||
(a, b) => b[0].length - a[0].length,
|
||||
)) {
|
||||
result = result.replaceAll(tok, String(val));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function createFormatter(spec) {
|
||||
return (v) => {
|
||||
@ -196,6 +171,11 @@ class BindingGraph {
|
||||
|
||||
// ─── String helpers ───
|
||||
|
||||
function skipWhitespace(str, pos) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
function skipString(str, i) {
|
||||
const q = str[i];
|
||||
i++;
|
||||
@ -260,18 +240,67 @@ function parseExpression(content) {
|
||||
return { expression: content, formatter: null };
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// Instruction classes — parser output
|
||||
// ─────────────────────────────────────────────────
|
||||
|
||||
class TextInstr {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class ExprInstr {
|
||||
constructor(expression, formatter) {
|
||||
this.expression = expression;
|
||||
this.formatter = formatter;
|
||||
}
|
||||
}
|
||||
|
||||
class ElementInstr {
|
||||
constructor(tag, attrs, children) {
|
||||
this.tag = tag;
|
||||
this.attrs = attrs;
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
|
||||
class AttrInstr {
|
||||
constructor(name, parts, bracket = null) {
|
||||
this.name = name;
|
||||
this.parts = parts;
|
||||
this.bracket = bracket; // null | "[]" | "[()]"
|
||||
}
|
||||
}
|
||||
|
||||
class ForInstr {
|
||||
constructor(varNames, expression, trackExpr, children, empty) {
|
||||
this.varNames = varNames;
|
||||
this.expression = expression;
|
||||
this.trackExpr = trackExpr;
|
||||
this.children = children;
|
||||
this.empty = empty;
|
||||
}
|
||||
}
|
||||
|
||||
class IfInstr {
|
||||
constructor(expression, trueChildren, falseChildren) {
|
||||
this.expression = expression;
|
||||
this.trueChildren = trueChildren;
|
||||
this.falseChildren = falseChildren;
|
||||
}
|
||||
}
|
||||
|
||||
class LetInstr {
|
||||
constructor(name, expression) {
|
||||
this.name = name;
|
||||
this.expression = expression;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// Parser — template string → instruction tree
|
||||
// ─────────────────────────────────────────────────
|
||||
//
|
||||
// Produces pure-data nodes. No DOM, no strings.
|
||||
//
|
||||
// { type: 'text', value }
|
||||
// { type: 'expr', expression, formatter }
|
||||
// { type: 'element', tag, attrs:[{name, parts}], children }
|
||||
// { type: 'for', varNames, expression, trackExpr, children, empty }
|
||||
// { type: 'if', expression, trueChildren, falseChildren }
|
||||
// { type: 'let', name, expression }
|
||||
|
||||
const VOID_ELEMENTS = new Set([
|
||||
"area",
|
||||
@ -366,13 +395,13 @@ function parseText(str, pos) {
|
||||
|
||||
if (str[pos] === "{" && str[pos + 1] === "{") {
|
||||
if (pos > start)
|
||||
nodes.push({ type: "text", value: str.substring(start, pos) });
|
||||
nodes.push(new TextInstr(str.substring(start, pos)));
|
||||
const end = str.indexOf("}}", pos + 2);
|
||||
if (end === -1) throw new Error("Unclosed {{ }}");
|
||||
const { expression, formatter } = parseExpression(
|
||||
str.substring(pos + 2, end).trim(),
|
||||
);
|
||||
nodes.push({ type: "expr", expression, formatter });
|
||||
nodes.push(new ExprInstr(expression, formatter));
|
||||
pos = end + 2;
|
||||
start = pos;
|
||||
continue;
|
||||
@ -382,7 +411,7 @@ function parseText(str, pos) {
|
||||
}
|
||||
|
||||
if (pos > start)
|
||||
nodes.push({ type: "text", value: str.substring(start, pos) });
|
||||
nodes.push(new TextInstr(str.substring(start, pos)));
|
||||
|
||||
return { nodes, pos };
|
||||
}
|
||||
@ -393,7 +422,7 @@ function parseAttrValue(str, pos) {
|
||||
let end = pos;
|
||||
while (end < str.length && !/[\s\/>]/.test(str[end])) end++;
|
||||
return {
|
||||
parts: [{ type: "text", value: str.substring(pos, end) }],
|
||||
parts: [new TextInstr(str.substring(pos, end))],
|
||||
pos: end,
|
||||
};
|
||||
}
|
||||
@ -405,13 +434,13 @@ function parseAttrValue(str, pos) {
|
||||
while (pos < str.length && str[pos] !== quote) {
|
||||
if (str[pos] === "{" && str[pos + 1] === "{") {
|
||||
if (pos > start)
|
||||
parts.push({ type: "text", value: str.substring(start, pos) });
|
||||
parts.push(new TextInstr(str.substring(start, pos)));
|
||||
const end = str.indexOf("}}", pos + 2);
|
||||
if (end === -1) throw new Error("Unclosed {{ }} in attribute");
|
||||
const { expression, formatter } = parseExpression(
|
||||
str.substring(pos + 2, end).trim(),
|
||||
);
|
||||
parts.push({ type: "expr", expression, formatter });
|
||||
parts.push(new ExprInstr(expression, formatter));
|
||||
pos = end + 2;
|
||||
start = pos;
|
||||
continue;
|
||||
@ -420,7 +449,7 @@ function parseAttrValue(str, pos) {
|
||||
}
|
||||
|
||||
if (pos > start)
|
||||
parts.push({ type: "text", value: str.substring(start, pos) });
|
||||
parts.push(new TextInstr(str.substring(start, pos)));
|
||||
|
||||
pos++; // closing quote
|
||||
return { parts, pos };
|
||||
@ -436,7 +465,7 @@ function parseElement(str, pos) {
|
||||
|
||||
const attrs = [];
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
pos = skipWhitespace(str, pos);
|
||||
|
||||
if (str[pos] === ">") {
|
||||
pos++;
|
||||
@ -444,32 +473,51 @@ function parseElement(str, pos) {
|
||||
}
|
||||
if (str[pos] === "/" && str[pos + 1] === ">") {
|
||||
pos += 2;
|
||||
return { node: { type: "element", tag, attrs, children: [] }, pos };
|
||||
return { node: new ElementInstr(tag, attrs, []), pos };
|
||||
}
|
||||
|
||||
// Attribute name — supports (event)="..." syntax
|
||||
// Attribute name — supports (event)="...", [attr]="...", [(attr)]="..." syntax
|
||||
let nameEnd = pos;
|
||||
while (nameEnd < str.length && !/[\s=\/>]/.test(str[nameEnd])) nameEnd++;
|
||||
const attrName = str.substring(pos, nameEnd);
|
||||
const rawAttrName = str.substring(pos, nameEnd);
|
||||
pos = nameEnd;
|
||||
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
// [attr]="expr" or [(attr)]="expr" — entire value is a single expression
|
||||
const bracketMatch = rawAttrName.match(/^\[(\(?\w[\w.\-]*\)?)]/);
|
||||
const isBracketExpr = bracketMatch !== null;
|
||||
const attrName = isBracketExpr ? bracketMatch[1] : rawAttrName;
|
||||
const bracket = isBracketExpr
|
||||
? (attrName.startsWith("(") && attrName.endsWith(")") ? "[()]" : "[]")
|
||||
: null;
|
||||
|
||||
pos = skipWhitespace(str, pos);
|
||||
|
||||
if (str[pos] !== "=") {
|
||||
attrs.push({ name: attrName, parts: [] }); // boolean attr
|
||||
attrs.push(new AttrInstr(attrName, [])); // boolean attr
|
||||
continue;
|
||||
}
|
||||
|
||||
pos++; // skip =
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
pos = skipWhitespace(str, pos);
|
||||
|
||||
const r = parseAttrValue(str, pos);
|
||||
attrs.push({ name: attrName, parts: r.parts });
|
||||
pos = r.pos;
|
||||
if (isBracketExpr) {
|
||||
const quote = str[pos];
|
||||
if (quote !== '"' && quote !== "'") throw new Error(`[${attrName}] requires a quoted value`);
|
||||
pos++; // skip opening quote
|
||||
const closeQuote = str.indexOf(quote, pos);
|
||||
if (closeQuote === -1) throw new Error(`Unclosed quote for [${attrName}]`);
|
||||
const { expression, formatter } = parseExpression(str.substring(pos, closeQuote).trim());
|
||||
attrs.push(new AttrInstr(attrName, [new ExprInstr(expression, formatter)], bracket));
|
||||
pos = closeQuote + 1;
|
||||
} else {
|
||||
const r = parseAttrValue(str, pos);
|
||||
attrs.push(new AttrInstr(attrName, r.parts));
|
||||
pos = r.pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (VOID_ELEMENTS.has(tag.toLowerCase())) {
|
||||
return { node: { type: "element", tag, attrs, children: [] }, pos };
|
||||
return { node: new ElementInstr(tag, attrs, []), pos };
|
||||
}
|
||||
|
||||
const { children, pos: childEnd } = parseChildren(str, pos);
|
||||
@ -481,7 +529,7 @@ function parseElement(str, pos) {
|
||||
if (closeEnd !== -1) pos = closeEnd + 1;
|
||||
}
|
||||
|
||||
return { node: { type: "element", tag, attrs, children }, pos };
|
||||
return { node: new ElementInstr(tag, attrs, children), pos };
|
||||
}
|
||||
|
||||
function parseFor(str, pos) {
|
||||
@ -521,14 +569,7 @@ function parseFor(str, pos) {
|
||||
}
|
||||
|
||||
return {
|
||||
node: {
|
||||
type: "for",
|
||||
varNames,
|
||||
expression: rest,
|
||||
trackExpr,
|
||||
children,
|
||||
empty,
|
||||
},
|
||||
node: new ForInstr(varNames, rest, trackExpr, children, empty),
|
||||
pos: afterBlock,
|
||||
};
|
||||
}
|
||||
@ -555,7 +596,7 @@ function parseIf(str, pos) {
|
||||
}
|
||||
|
||||
return {
|
||||
node: { type: "if", expression, trueChildren, falseChildren },
|
||||
node: new IfInstr(expression, trueChildren, falseChildren),
|
||||
pos: afterBlock,
|
||||
};
|
||||
}
|
||||
@ -567,11 +608,10 @@ function parseLet(str, pos) {
|
||||
const eqIdx = decl.indexOf("=");
|
||||
if (eqIdx === -1) throw new Error("@let missing =");
|
||||
return {
|
||||
node: {
|
||||
type: "let",
|
||||
name: decl.substring(0, eqIdx).trim(),
|
||||
expression: decl.substring(eqIdx + 1).trim(),
|
||||
},
|
||||
node: new LetInstr(
|
||||
decl.substring(0, eqIdx).trim(),
|
||||
decl.substring(eqIdx + 1).trim(),
|
||||
),
|
||||
pos: end + 1,
|
||||
};
|
||||
}
|
||||
@ -603,7 +643,7 @@ class AttrBinding {
|
||||
update(ctx) {
|
||||
let val = "";
|
||||
for (const p of this.parts) {
|
||||
if (p.type === "text") {
|
||||
if (p instanceof TextInstr) {
|
||||
val += p.value;
|
||||
} else {
|
||||
const raw = evaluate(ctx, p.expression);
|
||||
@ -617,6 +657,31 @@ class AttrBinding {
|
||||
}
|
||||
}
|
||||
|
||||
class AttrListBinding {
|
||||
constructor(element, attrName, attrListPart, expression) {
|
||||
this.element = element;
|
||||
this.attrName = attrName;
|
||||
this.attrListPart = attrListPart.trim();
|
||||
this.expression = expression;
|
||||
this.pattern = new RegExp(`\\b${attrListPart}\\b`);
|
||||
}
|
||||
|
||||
update(ctx) {
|
||||
const val = evaluate(ctx, this.expression);
|
||||
const attrList = (this.element.getAttribute(this.attrName) ?? "").trim();
|
||||
const match = this.pattern.exec(attrList);
|
||||
if (val && !match) {
|
||||
// works because upper trim() removes all whitespace, so empty string means no attributes
|
||||
this.element.setAttribute(this.attrName, attrList.length === 0 ? this.attrListPart : `${attrList} ${this.attrListPart}`);
|
||||
} else if (!val && match) {
|
||||
const start = match.index;
|
||||
const end = match.index + this.attrListPart.length;
|
||||
const newAttrList = attrList.slice(0, start) + attrList.slice(end);
|
||||
this.element.setAttribute(this.attrName, newAttrList.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LetBinding {
|
||||
constructor(name, expression) {
|
||||
this.name = name;
|
||||
@ -625,7 +690,7 @@ class LetBinding {
|
||||
declare(scope, ctx) {
|
||||
scope.set(this.name, evaluate(ctx, this.expression));
|
||||
}
|
||||
update() {}
|
||||
update() { }
|
||||
}
|
||||
|
||||
class ForBinding {
|
||||
@ -826,75 +891,85 @@ function instantiate(instructions, ctx, parentScope) {
|
||||
|
||||
function buildNodes(instructions, parent, ctx, bindings, graph) {
|
||||
for (const instr of instructions) {
|
||||
switch (instr.type) {
|
||||
case "text": {
|
||||
parent.appendChild(document.createTextNode(instr.value));
|
||||
break;
|
||||
}
|
||||
if (instr instanceof TextInstr) {
|
||||
parent.appendChild(document.createTextNode(instr.value));
|
||||
} else if (instr instanceof ExprInstr) {
|
||||
const node = document.createTextNode("");
|
||||
parent.appendChild(node);
|
||||
const b = new TextBinding(node, instr.expression, instr.formatter);
|
||||
bindings.push(b);
|
||||
graph.register(b, extractDeps(instr.expression));
|
||||
} else if (instr instanceof ElementInstr) {
|
||||
const el = document.createElement(instr.tag);
|
||||
|
||||
case "expr": {
|
||||
const node = document.createTextNode("");
|
||||
parent.appendChild(node);
|
||||
const b = new TextBinding(node, instr.expression, instr.formatter);
|
||||
bindings.push(b);
|
||||
graph.register(b, extractDeps(instr.expression));
|
||||
break;
|
||||
}
|
||||
for (const attr of instr.attrs) {
|
||||
// Bracket attrs ([]/[()]) → stash as metadata, not real DOM attrs
|
||||
if (attr.bracket) {
|
||||
if (!el.__templateMeta) el.__templateMeta = [];
|
||||
const expr = attr.parts.length === 1 && attr.parts[0] instanceof ExprInstr
|
||||
? attr.parts[0].expression : null;
|
||||
el.__templateMeta.push({ name: attr.name, bracket: attr.bracket, expression: expr });
|
||||
|
||||
case "element": {
|
||||
const el = document.createElement(instr.tag);
|
||||
|
||||
for (const attr of instr.attrs) {
|
||||
const hasDynamic = attr.parts.some((p) => p.type === "expr");
|
||||
if (!hasDynamic) {
|
||||
const val =
|
||||
attr.parts.length === 0
|
||||
? "" // boolean attr
|
||||
: attr.parts.map((p) => p.value).join("");
|
||||
el.setAttribute(attr.name, val);
|
||||
} else {
|
||||
// Still create bindings for [] attrs that affect rendering
|
||||
if (attr.bracket === "[]") {
|
||||
const deps = new Set();
|
||||
for (const p of attr.parts) {
|
||||
if (p.type === "expr")
|
||||
if (p instanceof ExprInstr) {
|
||||
for (const d of extractDeps(p.expression)) deps.add(d);
|
||||
}
|
||||
}
|
||||
|
||||
if (attr.name.includes(".") && attr.parts.length === 1 && attr.parts[0] instanceof ExprInstr) {
|
||||
const [attrName, attrListPart] = attr.name.split(".");
|
||||
const b = new AttrListBinding(el, attrName, attrListPart, attr.parts[0].expression);
|
||||
bindings.push(b);
|
||||
graph.register(b, deps);
|
||||
} else {
|
||||
const b = new AttrBinding(el, attr.name, attr.parts);
|
||||
bindings.push(b);
|
||||
graph.register(b, deps);
|
||||
}
|
||||
const b = new AttrBinding(el, attr.name, attr.parts);
|
||||
bindings.push(b);
|
||||
graph.register(b, deps);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
buildNodes(instr.children, el, ctx, bindings, graph);
|
||||
parent.appendChild(el);
|
||||
break;
|
||||
const hasDynamic = attr.parts.some((p) => p instanceof ExprInstr);
|
||||
if (!hasDynamic) {
|
||||
const val = attr.parts.length === 0 ? "" : attr.parts.map((p) => p.value).join("");
|
||||
el.setAttribute(attr.name, val);
|
||||
} else {
|
||||
const deps = new Set();
|
||||
for (const p of attr.parts) {
|
||||
if (p instanceof ExprInstr) {
|
||||
for (const d of extractDeps(p.expression)) deps.add(d);
|
||||
}
|
||||
}
|
||||
const b = new AttrBinding(el, attr.name, attr.parts);
|
||||
bindings.push(b);
|
||||
graph.register(b, deps);
|
||||
}
|
||||
}
|
||||
|
||||
case "for": {
|
||||
const start = document.createComment("for");
|
||||
const end = document.createComment("/for");
|
||||
parent.appendChild(start);
|
||||
parent.appendChild(end);
|
||||
const b = new ForBinding(start, end, instr);
|
||||
bindings.push(b);
|
||||
graph.register(b, extractDeps(instr.expression));
|
||||
break;
|
||||
}
|
||||
|
||||
case "if": {
|
||||
const start = document.createComment("if");
|
||||
const end = document.createComment("/if");
|
||||
parent.appendChild(start);
|
||||
parent.appendChild(end);
|
||||
const b = new IfBinding(start, end, instr);
|
||||
bindings.push(b);
|
||||
graph.register(b, extractDeps(instr.expression));
|
||||
break;
|
||||
}
|
||||
|
||||
case "let": {
|
||||
bindings.push(new LetBinding(instr.name, instr.expression));
|
||||
break;
|
||||
}
|
||||
buildNodes(instr.children, el, ctx, bindings, graph);
|
||||
parent.appendChild(el);
|
||||
} else if (instr instanceof ForInstr) {
|
||||
const start = document.createComment("for");
|
||||
const end = document.createComment("/for");
|
||||
parent.appendChild(start);
|
||||
parent.appendChild(end);
|
||||
const b = new ForBinding(start, end, instr);
|
||||
bindings.push(b);
|
||||
graph.register(b, extractDeps(instr.expression));
|
||||
} else if (instr instanceof IfInstr) {
|
||||
const start = document.createComment("if");
|
||||
const end = document.createComment("/if");
|
||||
parent.appendChild(start);
|
||||
parent.appendChild(end);
|
||||
const b = new IfBinding(start, end, instr);
|
||||
bindings.push(b);
|
||||
graph.register(b, extractDeps(instr.expression));
|
||||
} else if (instr instanceof LetInstr) {
|
||||
bindings.push(new LetBinding(instr.name, instr.expression));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,46 @@ class TestComponent extends Component {
|
||||
template: `<div>
|
||||
<h1>Test Component</h1>
|
||||
<p>This is a test component.</p>
|
||||
<button id="test-button" (click)="incrementCount()">Count: {{count()}}</button>
|
||||
<button
|
||||
id="test-button"
|
||||
[class.red-button]="count() % 2 === 0"
|
||||
[class.green-button]="count() % 2 !== 0"
|
||||
class="counter-button"
|
||||
(click)="incrementCount()"
|
||||
>Count: {{count()}}</button>
|
||||
<input type="text" [(value)]="name" placeholder="Enter your name" />
|
||||
<p>Hello, {{name()}}! Counter is {{count()}}</p>
|
||||
</div>`,
|
||||
style: `
|
||||
.red-button {
|
||||
background-color: red;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.green-button {
|
||||
background-color: green;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.counter-button {
|
||||
border-radius: 10px;
|
||||
margin: 15px 30px;
|
||||
font-size: 18px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.counter-button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
@ -24,6 +60,6 @@ class TestComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const route1 = new Route("counter", TestComponent);
|
||||
const route2 = new Route("counter/:count", TestComponent);
|
||||
const route1 = new Route("/counter", TestComponent);
|
||||
const route2 = new Route("/counter/:count", TestComponent);
|
||||
initRouter("routerOutlet", [route1, route2]);
|
||||
|
||||
@ -136,6 +136,7 @@ export class ComponentDefinition {
|
||||
templatesPath: null,
|
||||
template: null,
|
||||
stylesPath: null,
|
||||
style: null,
|
||||
scriptsPath: null,
|
||||
},
|
||||
) {
|
||||
@ -199,9 +200,9 @@ export class Component extends EventTarget {
|
||||
throw new Error("Component definition is not defined");
|
||||
}
|
||||
|
||||
onInit() {}
|
||||
onBeforeRender() {}
|
||||
onAfterRender() {}
|
||||
onInit() { }
|
||||
onBeforeRender() { }
|
||||
onAfterRender() { }
|
||||
onDestroy() {
|
||||
this._eventListeners.forEach((listener) => listener.detach());
|
||||
}
|
||||
|
||||
@ -14,8 +14,32 @@ class TestComponent extends Component {
|
||||
template: `<div>
|
||||
<h1>Test Component</h1>
|
||||
<p>This is a test component.</p>
|
||||
<button id="test-button" (click)="incrementCount()">Count: {{count}}</button>
|
||||
<button
|
||||
id="test-button"
|
||||
(click)="incrementCount()"
|
||||
[class.green-button]="count % 2 !== 0"
|
||||
[class.red-button]="count % 2 === 0"
|
||||
>Count: {{count}}</button>
|
||||
</div>`,
|
||||
style: `
|
||||
.red-button {
|
||||
background-color: red;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.green-button {
|
||||
background-color: green;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
@ -26,6 +50,6 @@ class TestComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const route1 = new Route("counter", TestComponent);
|
||||
const route2 = new Route("counter/:count", TestComponent);
|
||||
const route1 = new Route("/counter", TestComponent);
|
||||
const route2 = new Route("/counter/:count", TestComponent);
|
||||
initRouter("routerOutlet", "styleOutlet", [route1, route2]);
|
||||
|
||||
@ -152,6 +152,34 @@ export function parseDate(input, format) {
|
||||
return new Date(year, month - 1, day, hour, minute, second, ms);
|
||||
}
|
||||
|
||||
export function formatDate(date, spec) {
|
||||
const pad = (n, l = 2) => String(n).padStart(l, "0");
|
||||
const tokens = {
|
||||
yyyy: date.getFullYear(),
|
||||
yy: String(date.getFullYear()).slice(-2),
|
||||
MM: pad(date.getMonth() + 1),
|
||||
M: date.getMonth() + 1,
|
||||
dd: pad(date.getDate()),
|
||||
d: date.getDate(),
|
||||
HH: pad(date.getHours()),
|
||||
H: date.getHours(),
|
||||
hh: pad(date.getHours() % 12 || 12),
|
||||
h: date.getHours() % 12 || 12,
|
||||
mm: pad(date.getMinutes()),
|
||||
m: date.getMinutes(),
|
||||
ss: pad(date.getSeconds()),
|
||||
s: date.getSeconds(),
|
||||
fff: pad(date.getMilliseconds(), 3),
|
||||
};
|
||||
let result = spec;
|
||||
for (const [tok, val] of Object.entries(tokens).sort(
|
||||
(a, b) => b[0].length - a[0].length,
|
||||
)) {
|
||||
result = result.replaceAll(tok, String(val));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Input transform presets ───
|
||||
|
||||
export const IntInput = { transform: parseInt };
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
<body>
|
||||
<div>
|
||||
<h1>Test Site</h1>
|
||||
<a href="counter/1">Go to count 1</a>
|
||||
<a href="counter/2">Go to count 2</a>
|
||||
<a href="counter/3">Go to count 3</a>
|
||||
<a href="/counter/1">Go to count 1</a>
|
||||
<a href="/counter/2">Go to count 2</a>
|
||||
<a href="/counter/3">Go to count 3</a>
|
||||
<div id="routerOutlet">
|
||||
Hello There
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user