This commit is contained in:
nvms 2025-03-19 21:34:09 -04:00
parent e0f4945b1b
commit 02730daae5
9 changed files with 396 additions and 356 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules
.aider*

View File

@ -412,11 +412,12 @@ function _if(el, exp, ctx, component, componentProps, allProps) {
let block;
let activeBranchIndex = -1;
const removeActiveBlock = () => {
if (block) {
parent.insertBefore(anchor, block.element);
block.remove();
block = void 0;
if (!block) {
return;
}
parent.insertBefore(anchor, block.element);
block.remove();
block = void 0;
};
ctx.effect(() => {
for (let i = 0; i < branches.length; i++) {
@ -730,18 +731,6 @@ var $computed = Symbol("computed");
function isComputed(value) {
return isObject(value) && value[$computed];
}
function computed(getter) {
const ref2 = {
get value() {
return getter();
},
[$computed]: true
};
effect(() => {
getter();
});
return ref2;
}
// src/reactivity/ref.ts
var $ref = Symbol("ref");
@ -859,7 +848,7 @@ var App2 = class {
}
mount(component, target = "body", props = {}) {
const root = typeof target === "string" ? document.querySelector(target) : target;
const display = root.style.display;
const { display } = root.style;
root.style.display = "none";
this._mount(component, root, props);
root.style.display = display;
@ -873,7 +862,7 @@ var App2 = class {
}
parentContext.scope.$isRef = isRef;
parentContext.scope.$isComputed = isComputed;
const block = new Block({
return new Block({
app: this,
element: target,
parentContext,
@ -882,7 +871,6 @@ var App2 = class {
componentProps: props,
replacementType: "replaceChildren"
});
return block;
}
unmount() {
this.root.teardown();
@ -890,7 +878,7 @@ var App2 = class {
};
function createContext({ parentContext, app: app2 }) {
const context = {
app: app2 ? app2 : parentContext && parentContext.app ? parentContext.app : null,
app: app2 ? app2 : parentContext?.app ? parentContext.app : null,
scope: parentContext ? parentContext.scope : reactive({}),
blocks: [],
effects: [],
@ -944,7 +932,7 @@ function mergeProps(props, defaultProps) {
});
return merged;
}
var current = { componentBlock: void 0 };
var current2 = { componentBlock: void 0 };
var Block = class {
element;
context;
@ -962,17 +950,15 @@ var Block = class {
this.isFragment = opts.element instanceof HTMLTemplateElement;
this.parentComponentBlock = opts.parentComponentBlock;
if (opts.component) {
current.componentBlock = this;
current2.componentBlock = this;
this.element = stringToElement(opts.component.template);
} else if (this.isFragment) {
this.element = opts.element.content.cloneNode(true);
} else if (typeof opts.element === "string") {
this.element = stringToElement(opts.element);
} else {
if (this.isFragment) {
this.element = opts.element.content.cloneNode(true);
} else if (typeof opts.element === "string") {
this.element = stringToElement(opts.element);
} else {
this.element = opts.element.cloneNode(true);
opts.element.replaceWith(this.element);
}
this.element = opts.element.cloneNode(true);
opts.element.replaceWith(this.element);
}
if (opts.isRoot) {
this.context = opts.parentContext;
@ -1026,10 +1012,8 @@ var Block = class {
if (opts.element instanceof HTMLElement) {
opts.element.replaceWith(this.element);
}
} else {
if (opts.element instanceof HTMLElement) {
opts.element.replaceChildren(this.element);
}
} else if (opts.element instanceof HTMLElement) {
opts.element.replaceChildren(this.element);
}
}
}
@ -1111,112 +1095,111 @@ function walk(node, context) {
new InterpolationDirective({ element: node, context });
return;
}
if (isElement(node)) {
let exp;
const handleDirectives = (node2, context2, component, componentProps, allProps) => {
if (warnInvalidDirectives(node2, [":if", ":for"])) return;
if (warnInvalidDirectives(node2, [":for", ":teleport"])) return;
if (warnInvalidDirectives(node2, [":if", ":teleport"])) return;
if (exp = checkAndRemoveAttribute(node2, ":scope")) {
const scope = evalGet(context2.scope, exp, node2);
if (typeof scope === "object") {
Object.assign(context2.scope, scope);
}
}
if (exp = checkAndRemoveAttribute(node2, ":if")) {
return _if(node2, exp, context2, component, componentProps, allProps);
}
if (exp = checkAndRemoveAttribute(node2, ":for")) {
return _for(node2, exp, context2, component, componentProps, allProps);
}
if (exp = checkAndRemoveAttribute(node2, ":teleport")) {
return _teleport(node2, exp, context2, component, componentProps, allProps);
}
if (exp = checkAndRemoveAttribute(node2, ":show")) {
new ShowDirective({ element: node2, context: context2, expression: exp });
}
if (exp = checkAndRemoveAttribute(node2, ":ref")) {
context2.scope[exp].value = node2;
}
if (exp = checkAndRemoveAttribute(node2, ":value")) {
new ValueDirective({ element: node2, context: context2, expression: exp });
}
if (exp = checkAndRemoveAttribute(node2, ":html")) {
const htmlExp = exp;
context2.effect(() => {
const result = evalGet(context2.scope, htmlExp, node2);
if (result instanceof Element) {
node2.replaceChildren();
node2.append(result);
} else {
node2.innerHTML = result;
}
});
}
if (exp = checkAndRemoveAttribute(node2, ":text")) {
const textExp = exp;
context2.effect(() => {
node2.textContent = toDisplayString(evalGet(context2.scope, textExp, node2));
});
}
};
const processAttributes = (node2, component) => {
return Array.from(node2.attributes).filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)).map((attr) => ({
isMirror: isMirrorProp(attr.name),
isSpread: isSpreadProp(attr.name),
isBind: attr.name.includes("bind"),
originalName: attr.name,
extractedName: extractPropName(attr.name),
exp: attr.value,
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node2) : attr.value ? evalGet(context.scope, attr.value, node2) : void 0
}));
};
if (isComponent(node, context)) {
const component = context.app.getComponent(node.tagName.toLowerCase());
const allProps = processAttributes(node, component);
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
if (isSpread) {
const spread = evalGet(context.scope, extractedName, node);
if (isObject(spread)) Object.assign(acc, spread);
} else if (isMirror) {
acc[extractedName] = evalGet(context.scope, extractedName, node);
} else {
acc[extractedName] = value;
}
return acc;
}, {});
const next2 = handleDirectives(node, context, component, componentProps, allProps);
if (next2) return next2;
const templates = findTemplateNodes(node);
return new Block({
element: node,
app: current.componentBlock.context.app,
// parentContext: context,
component,
replacementType: "replace",
parentComponentBlock: current.componentBlock,
templates,
componentProps,
allProps
}).element;
}
const next = handleDirectives(node, context);
if (next) return next;
Array.from(node.attributes).forEach((attr) => {
if (isPropAttribute(attr.name)) {
new AttributeDirective({ element: node, context, attr });
}
if (isEventAttribute(attr.name)) {
new EventDirective({ element: node, context, attr });
}
});
walkChildren(node, context);
if (!isElement(node)) {
return;
}
let exp;
const handleDirectives = (node2, context2, component, componentProps, allProps) => {
if (warnInvalidDirectives(node2, [":if", ":for"])) return;
if (warnInvalidDirectives(node2, [":for", ":teleport"])) return;
if (warnInvalidDirectives(node2, [":if", ":teleport"])) return;
if (exp = checkAndRemoveAttribute(node2, ":scope")) {
const scope = evalGet(context2.scope, exp, node2);
if (typeof scope === "object") {
Object.assign(context2.scope, scope);
}
}
if (exp = checkAndRemoveAttribute(node2, ":if")) {
return _if(node2, exp, context2, component, componentProps, allProps);
}
if (exp = checkAndRemoveAttribute(node2, ":for")) {
return _for(node2, exp, context2, component, componentProps, allProps);
}
if (exp = checkAndRemoveAttribute(node2, ":teleport")) {
return _teleport(node2, exp, context2, component, componentProps, allProps);
}
if (exp = checkAndRemoveAttribute(node2, ":show")) {
new ShowDirective({ element: node2, context: context2, expression: exp });
}
if (exp = checkAndRemoveAttribute(node2, ":ref")) {
context2.scope[exp].value = node2;
}
if (exp = checkAndRemoveAttribute(node2, ":value")) {
new ValueDirective({ element: node2, context: context2, expression: exp });
}
if (exp = checkAndRemoveAttribute(node2, ":html")) {
const htmlExp = exp;
context2.effect(() => {
const result = evalGet(context2.scope, htmlExp, node2);
if (result instanceof Element) {
node2.replaceChildren();
node2.append(result);
} else {
node2.innerHTML = result;
}
});
}
if (exp = checkAndRemoveAttribute(node2, ":text")) {
const textExp = exp;
context2.effect(() => {
node2.textContent = toDisplayString(evalGet(context2.scope, textExp, node2));
});
}
};
const processAttributes = (node2, component) => Array.from(node2.attributes).filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)).map((attr) => ({
isMirror: isMirrorProp(attr.name),
isSpread: isSpreadProp(attr.name),
isBind: attr.name.includes("bind"),
originalName: attr.name,
extractedName: extractPropName(attr.name),
exp: attr.value,
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node2) : attr.value ? evalGet(context.scope, attr.value, node2) : void 0
}));
if (isComponent(node, context)) {
const component = context.app.getComponent(node.tagName.toLowerCase());
const allProps = processAttributes(node, component);
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
if (isSpread) {
const spread = evalGet(context.scope, extractedName, node);
if (isObject(spread)) Object.assign(acc, spread);
} else if (isMirror) {
acc[extractedName] = evalGet(context.scope, extractedName, node);
} else {
acc[extractedName] = value;
}
return acc;
}, {});
const next2 = handleDirectives(node, context, component, componentProps, allProps);
if (next2) return next2;
const templates = findTemplateNodes(node);
return new Block({
element: node,
app: current2.componentBlock.context.app,
// parentContext: context,
component,
replacementType: "replace",
parentComponentBlock: current2.componentBlock,
templates,
componentProps,
allProps
}).element;
}
const next = handleDirectives(node, context);
if (next) return next;
Array.from(node.attributes).forEach((attr) => {
if (isPropAttribute(attr.name)) {
new AttributeDirective({ element: node, context, attr });
}
if (isEventAttribute(attr.name)) {
new EventDirective({ element: node, context, attr });
}
});
walkChildren(node, context);
}
function walkChildren(node, context) {
let child = node.firstChild;
while (child) {
child = walk(child, context) || child.nextSibling;
let child2 = node.firstChild;
while (child2) {
child2 = walk(child2, context) || child2.nextSibling;
}
}
var evalFuncCache = {};
@ -1251,38 +1234,41 @@ function flattenRefs(scope) {
const mapped = {};
for (const key in scope) {
if (scope.hasOwnProperty(key)) {
if (isRef(scope[key])) {
mapped[key] = scope[key].value;
} else {
mapped[key] = scope[key];
}
mapped[key] = isRef(scope[key]) ? scope[key].value : scope[key];
}
}
return mapped;
}
// src/demo.ts
var main = {
var child = {
template: html`
<div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
<h1 class="f1 margin-bottom-1 color-60">phase</h1>
<h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
<grid columns="6" class="f6 white-space-nowrap">
<div :for="variant in ['base', 'accent', 'red', 'rose', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'lavender', 'violet', 'purple', 'plum', 'fuchsia', 'pink', 'peach']" class="border-color-30 border-2px">
<div :for="rank, index in ranks">
<div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
</div>
</div>
</grid>
<div>
I am child and I have a cheeseburger: "{{food}}" (does not inherit)
<div>
<slot />
</div>
</div>
`,
main() {
const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
const basesReverse = computed(() => Array.from(ranks).reverse());
const bg = (variant, rank, index) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
return { ranks, bg };
const food = ref("\u{1F354}");
return { food };
}
};
var main = {
template: html`
<div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
<div :scope="{ food: '🍕' }">
<div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div>
<child>Child slot, food: {{food}} {{drink}}</child>
</div>
</div>
`,
main() {
return { food: ref("nothing") };
}
};
var app = new App2();
app.register("child", child);
app.mount(main, "#app");
//# sourceMappingURL=demo.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
import { App } from ".";
import { computed } from "./reactivity/computed";
import { reactive } from "./reactivity/reactive";
import { ref } from "./reactivity/ref";
import { html } from "./util";
// ------------------------------------------------
@ -188,15 +189,15 @@ import { html } from "./util";
// const style = reactive({ color: "gray" });
// const increment = () => count.value++;
// const decrement = () => count.value--;
//
// setInterval(() => {
// style.color = style.color === "gray" ? "white" : "gray";
// }, 500);
//
// return { count, increment, decrement, style };
// },
// };
//
// const app = new App();
// app.mount(counter, "#app");
@ -219,11 +220,11 @@ import { html } from "./util";
// main() {
// const items = reactive([1, 2, 3, 4, 5]);
// const bool = ref(true);
// setInterval(() => (bool.value = !bool.value), 250);
// setInterval(() => (bool.value = !bool.value), 2050);
// return { items, bool };
// },
// };
//
// const app = new App();
// app.mount(main, "#app");
@ -249,67 +250,67 @@ import { html } from "./util";
// ------------------------------------------------
// Colors from css framework
const main = {
template: html`
<div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
<h1 class="f1 margin-bottom-1 color-60">phase</h1>
<h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
<grid columns="6" class="f6 white-space-nowrap">
<div :for="variant in ['base', 'accent', 'red', 'rose', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'lavender', 'violet', 'purple', 'plum', 'fuchsia', 'pink', 'peach']" class="border-color-30 border-2px">
<div :for="rank, index in ranks">
<div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
</div>
</div>
</grid>
</div>
`,
main() {
const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
const basesReverse = computed(() => Array.from(ranks).reverse());
const bg = (variant: string, rank: string, index: number) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
return { ranks, bg };
},
};
const app = new App();
app.mount(main, "#app");
// ------------------------------------------------
// :scope
// const child = {
// template: html`
// <div>
// I am child and I have food: "{{food}}" (does not inherit)
// <div>
// <slot />
// </div>
// </div>
// `,
// main() {
// const food = ref("🍔");
// return { food };
// },
// };
// const main = {
// template: html`
// <div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
// <div :scope="{ food: '🍕' }">
// <div>Scoped food: {{food}} and scoped drink: {{drink}}</div>
// <child>Child slot, food: {{food}} {{drink}}</child>
// </div>
// <div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
// <h1 class="f1 margin-bottom-1 color-60">phase</h1>
// <h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
// <grid columns="6" class="f6 white-space-nowrap">
// <div :for="variant in ['base', 'accent', 'red', 'rose', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'lavender', 'violet', 'purple', 'plum', 'fuchsia', 'pink', 'peach']" class="border-color-30 border-2px">
// <div :for="rank, index in ranks">
// <div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
// </div>
// </div>
// </grid>
// </div>
// `,
// main() {
// return { food: ref("nothing") };
// const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
// const basesReverse = computed(() => Array.from(ranks).reverse());
// const bg = (variant: string, rank: string, index: number) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
// return { ranks, bg };
// },
// };
// const app = new App();
// app.register("child", child);
// app.mount(main, "#app");
// ------------------------------------------------
// :scope
const child = {
template: html`
<div>
I am child and I have a cheeseburger: "{{food}}" (does not inherit)
<div>
<slot />
</div>
</div>
`,
main() {
const food = ref("🍔");
return { food };
},
};
const main = {
template: html`
<div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
<div :scope="{ food: '🍕' }">
<div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div>
<child>Child slot, food: {{food}} {{drink}}</child>
</div>
</div>
`,
main() {
return { food: ref("nothing") };
},
};
const app = new App();
app.register("child", child);
app.mount(main, "#app");
// ------------------------------------------------
// Practical :scope demo
// const main = {

View File

@ -35,11 +35,11 @@ export function _if(el: Element, exp: string, ctx: Context, component?: Componen
let activeBranchIndex = -1;
const removeActiveBlock = () => {
if (block) {
parent.insertBefore(anchor, block.element);
block.remove();
block = undefined;
}
if (!block) { return; }
parent.insertBefore(anchor, block.element);
block.remove();
block = undefined;
};
ctx.effect(() => {

View File

@ -1,4 +1,4 @@
import { Context, evalGet } from "../";
import { Context, current, evalGet } from "../";
import { insertAfter, toDisplayString } from "../util";
interface InterpolationDirectiveOptions {

View File

@ -80,7 +80,7 @@ export class App {
mount(component: Component, target: string | HTMLElement = "body", props: Record<string, any> = {}) {
const root = typeof target === "string" ? (document.querySelector(target) as HTMLElement) : target;
const display = root.style.display;
const { display } = root.style;
root.style.display = "none";
this._mount(component, root, props);
root.style.display = display;
@ -98,7 +98,7 @@ export class App {
parentContext.scope.$isRef = isRef;
parentContext.scope.$isComputed = isComputed;
const block = new Block({
return new Block({
app: this,
element: target,
parentContext,
@ -107,8 +107,6 @@ export class App {
componentProps: props,
replacementType: "replaceChildren",
});
return block;
}
unmount() {
@ -134,7 +132,7 @@ interface CreateContextOptions {
export function createContext({ parentContext, app }: CreateContextOptions): Context {
const context: Context = {
app: app ? app : parentContext && parentContext.app ? parentContext.app : null,
app: app ? app : parentContext?.app ? parentContext.app : null,
scope: parentContext ? parentContext.scope : reactive({}),
blocks: [],
effects: [],
@ -251,15 +249,13 @@ export class Block {
if (opts.component) {
current.componentBlock = this;
this.element = stringToElement(opts.component.template);
} else if (this.isFragment) {
this.element = (opts.element as HTMLTemplateElement).content.cloneNode(true) as Element;
} else if (typeof opts.element === "string") {
this.element = stringToElement(opts.element);
} else {
if (this.isFragment) {
this.element = (opts.element as HTMLTemplateElement).content.cloneNode(true) as Element;
} else if (typeof opts.element === "string") {
this.element = stringToElement(opts.element);
} else {
this.element = opts.element.cloneNode(true) as Element;
opts.element.replaceWith(this.element);
}
this.element = opts.element.cloneNode(true) as Element;
opts.element.replaceWith(this.element);
}
if (opts.isRoot) {
@ -326,10 +322,8 @@ export class Block {
if (opts.element instanceof HTMLElement) {
opts.element.replaceWith(this.element);
}
} else {
if (opts.element instanceof HTMLElement) {
opts.element.replaceChildren(this.element);
}
} else if (opts.element instanceof HTMLElement) {
opts.element.replaceChildren(this.element);
}
}
}
@ -432,128 +426,126 @@ function walk(node: Node, context: Context) {
return;
}
if (isElement(node)) {
let exp: string | null;
if (!isElement(node)) { return; }
const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => {
if (warnInvalidDirectives(node, [":if", ":for"])) return;
if (warnInvalidDirectives(node, [":for", ":teleport"])) return;
if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
let exp: string | null;
// e.g. <div :scope="{ open: true }" />
// In this case, the scope is merged into context.scope and will overwrite
// anything returned from `main`.
if ((exp = checkAndRemoveAttribute(node, ":scope"))) {
const scope = evalGet(context.scope, exp, node);
if (typeof scope === "object") {
Object.assign(context.scope, scope);
// context = createScopedContext(context, scope);
}
const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => {
if (warnInvalidDirectives(node, [":if", ":for"])) return;
if (warnInvalidDirectives(node, [":for", ":teleport"])) return;
if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
// e.g. <div :scope="{ open: true }" />
// In this case, the scope is merged into context.scope and will overwrite
// anything returned from `main`.
if ((exp = checkAndRemoveAttribute(node, ":scope"))) {
const scope = evalGet(context.scope, exp, node);
if (typeof scope === "object") {
Object.assign(context.scope, scope);
// context = createScopedContext(context, scope);
}
if ((exp = checkAndRemoveAttribute(node, ":if"))) {
return _if(node, exp, context, component, componentProps, allProps);
}
if ((exp = checkAndRemoveAttribute(node, ":for"))) {
return _for(node, exp, context, component, componentProps, allProps);
}
if ((exp = checkAndRemoveAttribute(node, ":teleport"))) {
return _teleport(node, exp, context, component, componentProps, allProps);
}
if ((exp = checkAndRemoveAttribute(node, ":show"))) {
new ShowDirective({ element: node, context, expression: exp });
}
if ((exp = checkAndRemoveAttribute(node, ":ref"))) {
context.scope[exp].value = node;
}
if ((exp = checkAndRemoveAttribute(node, ":value"))) {
new ValueDirective({ element: node, context, expression: exp });
}
if ((exp = checkAndRemoveAttribute(node, ":html"))) {
const htmlExp = exp;
context.effect(() => {
const result = evalGet(context.scope, htmlExp, node);
if (result instanceof Element) {
node.replaceChildren();
node.append(result);
} else {
node.innerHTML = result;
}
});
}
if ((exp = checkAndRemoveAttribute(node, ":text"))) {
const textExp = exp;
context.effect(() => {
node.textContent = toDisplayString(evalGet(context.scope, textExp, node));
});
}
};
const processAttributes = (node: Element, component?: Component) => {
return Array.from(node.attributes)
.filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || (isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)))
.map((attr) => ({
isMirror: isMirrorProp(attr.name),
isSpread: isSpreadProp(attr.name),
isBind: attr.name.includes("bind"),
originalName: attr.name,
extractedName: extractPropName(attr.name),
exp: attr.value,
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node) : attr.value ? evalGet(context.scope, attr.value, node) : undefined,
}));
};
if (isComponent(node, context)) {
const component = context.app.getComponent(node.tagName.toLowerCase());
const allProps = processAttributes(node, component);
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
if (isSpread) {
const spread = evalGet(context.scope, extractedName, node);
if (isObject(spread)) Object.assign(acc, spread);
} else if (isMirror) {
acc[extractedName] = evalGet(context.scope, extractedName, node);
} else {
acc[extractedName] = value;
}
return acc;
}, {});
const next = handleDirectives(node, context, component, componentProps, allProps);
if (next) return next;
const templates = findTemplateNodes(node);
return new Block({
element: node,
app: current.componentBlock.context.app,
// parentContext: context,
component,
replacementType: "replace",
parentComponentBlock: current.componentBlock,
templates,
componentProps,
allProps,
}).element;
}
const next = handleDirectives(node, context);
if ((exp = checkAndRemoveAttribute(node, ":if"))) {
return _if(node, exp, context, component, componentProps, allProps);
}
if ((exp = checkAndRemoveAttribute(node, ":for"))) {
return _for(node, exp, context, component, componentProps, allProps);
}
if ((exp = checkAndRemoveAttribute(node, ":teleport"))) {
return _teleport(node, exp, context, component, componentProps, allProps);
}
if ((exp = checkAndRemoveAttribute(node, ":show"))) {
new ShowDirective({ element: node, context, expression: exp });
}
if ((exp = checkAndRemoveAttribute(node, ":ref"))) {
context.scope[exp].value = node;
}
if ((exp = checkAndRemoveAttribute(node, ":value"))) {
new ValueDirective({ element: node, context, expression: exp });
}
if ((exp = checkAndRemoveAttribute(node, ":html"))) {
const htmlExp = exp;
context.effect(() => {
const result = evalGet(context.scope, htmlExp, node);
if (result instanceof Element) {
node.replaceChildren();
node.append(result);
} else {
node.innerHTML = result;
}
});
}
if ((exp = checkAndRemoveAttribute(node, ":text"))) {
const textExp = exp;
context.effect(() => {
node.textContent = toDisplayString(evalGet(context.scope, textExp, node));
});
}
};
const processAttributes = (node: Element, component?: Component) => Array.from(node.attributes)
.filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || (isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)))
.map((attr) => ({
isMirror: isMirrorProp(attr.name),
isSpread: isSpreadProp(attr.name),
isBind: attr.name.includes("bind"),
originalName: attr.name,
extractedName: extractPropName(attr.name),
exp: attr.value,
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node) : attr.value ? evalGet(context.scope, attr.value, node) : undefined,
}));
if (isComponent(node, context)) {
const component = context.app.getComponent(node.tagName.toLowerCase());
const allProps = processAttributes(node, component);
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
if (isSpread) {
const spread = evalGet(context.scope, extractedName, node);
if (isObject(spread)) Object.assign(acc, spread);
} else if (isMirror) {
acc[extractedName] = evalGet(context.scope, extractedName, node);
} else {
acc[extractedName] = value;
}
return acc;
}, {});
const next = handleDirectives(node, context, component, componentProps, allProps);
if (next) return next;
Array.from(node.attributes).forEach((attr) => {
if (isPropAttribute(attr.name)) {
new AttributeDirective({ element: node, context, attr });
}
const templates = findTemplateNodes(node);
if (isEventAttribute(attr.name)) {
new EventDirective({ element: node, context, attr });
}
});
walkChildren(node, context);
return new Block({
element: node,
app: current.componentBlock.context.app,
// parentContext: context,
component,
replacementType: "replace",
parentComponentBlock: current.componentBlock,
templates,
componentProps,
allProps,
}).element;
}
const next = handleDirectives(node, context);
if (next) return next;
Array.from(node.attributes).forEach((attr) => {
if (isPropAttribute(attr.name)) {
new AttributeDirective({ element: node, context, attr });
}
if (isEventAttribute(attr.name)) {
new EventDirective({ element: node, context, attr });
}
});
walkChildren(node, context);
}
function walkChildren(node: Node, context: Context) {
@ -605,11 +597,7 @@ function flattenRefs(scope: any): any {
for (const key in scope) {
if (scope.hasOwnProperty(key)) {
// Check if the value is a Ref
if (isRef(scope[key])) {
mapped[key] = scope[key].value;
} else {
mapped[key] = scope[key];
}
mapped[key] = isRef(scope[key]) ? scope[key].value : scope[key];
}
}
return mapped;

26
tests/app.test.ts Normal file
View File

@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest";
import { App, Component } from "../src/index";
import { reactive } from "../src/reactivity/reactive";
describe("App", () => {
it("should mount a simple component and update reactive data", () => {
const app = new App();
const component: Component = {
template: "<div>{{ message }}</div>",
props: { message: { default: "Hello" } },
main(props) {
return reactive({ message: props.message });
},
};
const root = document.createElement("div");
app.mount(component, root);
expect(root.innerHTML).toBe("<div>Hello</div>");
const { scope } = app.root.context;
scope.message = "World";
expect(root.innerHTML).toBe("<div>World</div>");
});
});

38
tests/router.test.ts Normal file
View File

@ -0,0 +1,38 @@
import { describe, it, expect, beforeEach } from "vitest";
import { App } from "../src/index";
import { RouterPlugin } from "../src/plugins/router";
describe("RouterPlugin", () => {
let app: App;
let router: RouterPlugin;
beforeEach(() => {
app = new App();
router = new RouterPlugin([
{
path: "/",
component: { template: "<div>Home</div>" },
},
{
path: "/about",
component: { template: "<div>About</div>" },
},
]);
app.use(router);
});
it("should render the correct component on route change", () => {
const root = document.createElement("div");
root.innerHTML = "<router-view></router-view>";
document.body.appendChild(root);
router.compile(root.firstElementChild as Element);
router.doRouteChange("/");
expect(root.innerHTML).toContain("<div>Home</div>");
router.doRouteChange("/about");
expect(root.innerHTML).toContain("<div>About</div>");
});
});