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 node_modules
.aider*

View File

@ -412,11 +412,12 @@ function _if(el, exp, ctx, component, componentProps, allProps) {
let block; let block;
let activeBranchIndex = -1; let activeBranchIndex = -1;
const removeActiveBlock = () => { const removeActiveBlock = () => {
if (block) { if (!block) {
parent.insertBefore(anchor, block.element); return;
block.remove();
block = void 0;
} }
parent.insertBefore(anchor, block.element);
block.remove();
block = void 0;
}; };
ctx.effect(() => { ctx.effect(() => {
for (let i = 0; i < branches.length; i++) { for (let i = 0; i < branches.length; i++) {
@ -730,18 +731,6 @@ var $computed = Symbol("computed");
function isComputed(value) { function isComputed(value) {
return isObject(value) && value[$computed]; return isObject(value) && value[$computed];
} }
function computed(getter) {
const ref2 = {
get value() {
return getter();
},
[$computed]: true
};
effect(() => {
getter();
});
return ref2;
}
// src/reactivity/ref.ts // src/reactivity/ref.ts
var $ref = Symbol("ref"); var $ref = Symbol("ref");
@ -859,7 +848,7 @@ var App2 = class {
} }
mount(component, target = "body", props = {}) { mount(component, target = "body", props = {}) {
const root = typeof target === "string" ? document.querySelector(target) : target; const root = typeof target === "string" ? document.querySelector(target) : target;
const display = root.style.display; const { display } = root.style;
root.style.display = "none"; root.style.display = "none";
this._mount(component, root, props); this._mount(component, root, props);
root.style.display = display; root.style.display = display;
@ -873,7 +862,7 @@ var App2 = class {
} }
parentContext.scope.$isRef = isRef; parentContext.scope.$isRef = isRef;
parentContext.scope.$isComputed = isComputed; parentContext.scope.$isComputed = isComputed;
const block = new Block({ return new Block({
app: this, app: this,
element: target, element: target,
parentContext, parentContext,
@ -882,7 +871,6 @@ var App2 = class {
componentProps: props, componentProps: props,
replacementType: "replaceChildren" replacementType: "replaceChildren"
}); });
return block;
} }
unmount() { unmount() {
this.root.teardown(); this.root.teardown();
@ -890,7 +878,7 @@ var App2 = class {
}; };
function createContext({ parentContext, app: app2 }) { function createContext({ parentContext, app: app2 }) {
const context = { const context = {
app: app2 ? app2 : parentContext && parentContext.app ? parentContext.app : null, app: app2 ? app2 : parentContext?.app ? parentContext.app : null,
scope: parentContext ? parentContext.scope : reactive({}), scope: parentContext ? parentContext.scope : reactive({}),
blocks: [], blocks: [],
effects: [], effects: [],
@ -944,7 +932,7 @@ function mergeProps(props, defaultProps) {
}); });
return merged; return merged;
} }
var current = { componentBlock: void 0 }; var current2 = { componentBlock: void 0 };
var Block = class { var Block = class {
element; element;
context; context;
@ -962,17 +950,15 @@ var Block = class {
this.isFragment = opts.element instanceof HTMLTemplateElement; this.isFragment = opts.element instanceof HTMLTemplateElement;
this.parentComponentBlock = opts.parentComponentBlock; this.parentComponentBlock = opts.parentComponentBlock;
if (opts.component) { if (opts.component) {
current.componentBlock = this; current2.componentBlock = this;
this.element = stringToElement(opts.component.template); 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 { } else {
if (this.isFragment) { this.element = opts.element.cloneNode(true);
this.element = opts.element.content.cloneNode(true); opts.element.replaceWith(this.element);
} else if (typeof opts.element === "string") {
this.element = stringToElement(opts.element);
} else {
this.element = opts.element.cloneNode(true);
opts.element.replaceWith(this.element);
}
} }
if (opts.isRoot) { if (opts.isRoot) {
this.context = opts.parentContext; this.context = opts.parentContext;
@ -1026,10 +1012,8 @@ var Block = class {
if (opts.element instanceof HTMLElement) { if (opts.element instanceof HTMLElement) {
opts.element.replaceWith(this.element); opts.element.replaceWith(this.element);
} }
} else { } else if (opts.element instanceof HTMLElement) {
if (opts.element instanceof HTMLElement) { opts.element.replaceChildren(this.element);
opts.element.replaceChildren(this.element);
}
} }
} }
} }
@ -1111,112 +1095,111 @@ function walk(node, context) {
new InterpolationDirective({ element: node, context }); new InterpolationDirective({ element: node, context });
return; return;
} }
if (isElement(node)) { if (!isElement(node)) {
let exp; return;
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);
} }
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) { function walkChildren(node, context) {
let child = node.firstChild; let child2 = node.firstChild;
while (child) { while (child2) {
child = walk(child, context) || child.nextSibling; child2 = walk(child2, context) || child2.nextSibling;
} }
} }
var evalFuncCache = {}; var evalFuncCache = {};
@ -1251,38 +1234,41 @@ function flattenRefs(scope) {
const mapped = {}; const mapped = {};
for (const key in scope) { for (const key in scope) {
if (scope.hasOwnProperty(key)) { if (scope.hasOwnProperty(key)) {
if (isRef(scope[key])) { mapped[key] = isRef(scope[key]) ? scope[key].value : scope[key];
mapped[key] = scope[key].value;
} else {
mapped[key] = scope[key];
}
} }
} }
return mapped; return mapped;
} }
// src/demo.ts // src/demo.ts
var main = { var child = {
template: html` template: html`
<div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;"> <div>
<h1 class="f1 margin-bottom-1 color-60">phase</h1> I am child and I have a cheeseburger: "{{food}}" (does not inherit)
<h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2> <div>
<grid columns="6" class="f6 white-space-nowrap"> <slot />
<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>
<div :for="rank, index in ranks">
<div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
</div>
</div>
</grid>
</div> </div>
`, `,
main() { main() {
const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]); const food = ref("\u{1F354}");
const basesReverse = computed(() => Array.from(ranks).reverse()); return { food };
const bg = (variant, rank, index) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` }); }
return { ranks, bg }; };
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(); var app = new App2();
app.register("child", child);
app.mount(main, "#app"); app.mount(main, "#app");
//# sourceMappingURL=demo.js.map //# 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 { App } from ".";
import { computed } from "./reactivity/computed"; import { computed } from "./reactivity/computed";
import { reactive } from "./reactivity/reactive"; import { reactive } from "./reactivity/reactive";
import { ref } from "./reactivity/ref";
import { html } from "./util"; import { html } from "./util";
// ------------------------------------------------ // ------------------------------------------------
@ -188,15 +189,15 @@ import { html } from "./util";
// const style = reactive({ color: "gray" }); // const style = reactive({ color: "gray" });
// const increment = () => count.value++; // const increment = () => count.value++;
// const decrement = () => count.value--; // const decrement = () => count.value--;
//
// setInterval(() => { // setInterval(() => {
// style.color = style.color === "gray" ? "white" : "gray"; // style.color = style.color === "gray" ? "white" : "gray";
// }, 500); // }, 500);
//
// return { count, increment, decrement, style }; // return { count, increment, decrement, style };
// }, // },
// }; // };
//
// const app = new App(); // const app = new App();
// app.mount(counter, "#app"); // app.mount(counter, "#app");
@ -219,11 +220,11 @@ import { html } from "./util";
// main() { // main() {
// const items = reactive([1, 2, 3, 4, 5]); // const items = reactive([1, 2, 3, 4, 5]);
// const bool = ref(true); // const bool = ref(true);
// setInterval(() => (bool.value = !bool.value), 250); // setInterval(() => (bool.value = !bool.value), 2050);
// return { items, bool }; // return { items, bool };
// }, // },
// }; // };
//
// const app = new App(); // const app = new App();
// app.mount(main, "#app"); // app.mount(main, "#app");
@ -249,67 +250,67 @@ import { html } from "./util";
// ------------------------------------------------ // ------------------------------------------------
// Colors from css framework // 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 = { // const main = {
// template: html` // template: html`
// <div class="hero sans-serif f2" :scope="{ drink: '🍹' }"> // <div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
// <div :scope="{ food: '🍕' }"> // <h1 class="f1 margin-bottom-1 color-60">phase</h1>
// <div>Scoped food: {{food}} and scoped drink: {{drink}}</div> // <h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
// <child>Child slot, food: {{food}} {{drink}}</child> // <grid columns="6" class="f6 white-space-nowrap">
// </div> // <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> // </div>
// `, // `,
// main() { // 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(); // const app = new App();
// app.register("child", child);
// app.mount(main, "#app"); // 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 // Practical :scope demo
// const main = { // const main = {

View File

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

View File

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

View File

@ -80,7 +80,7 @@ export class App {
mount(component: Component, target: string | HTMLElement = "body", props: Record<string, any> = {}) { mount(component: Component, target: string | HTMLElement = "body", props: Record<string, any> = {}) {
const root = typeof target === "string" ? (document.querySelector(target) as HTMLElement) : target; const root = typeof target === "string" ? (document.querySelector(target) as HTMLElement) : target;
const display = root.style.display; const { display } = root.style;
root.style.display = "none"; root.style.display = "none";
this._mount(component, root, props); this._mount(component, root, props);
root.style.display = display; root.style.display = display;
@ -98,7 +98,7 @@ export class App {
parentContext.scope.$isRef = isRef; parentContext.scope.$isRef = isRef;
parentContext.scope.$isComputed = isComputed; parentContext.scope.$isComputed = isComputed;
const block = new Block({ return new Block({
app: this, app: this,
element: target, element: target,
parentContext, parentContext,
@ -107,8 +107,6 @@ export class App {
componentProps: props, componentProps: props,
replacementType: "replaceChildren", replacementType: "replaceChildren",
}); });
return block;
} }
unmount() { unmount() {
@ -134,7 +132,7 @@ interface CreateContextOptions {
export function createContext({ parentContext, app }: CreateContextOptions): Context { export function createContext({ parentContext, app }: CreateContextOptions): Context {
const context: 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({}), scope: parentContext ? parentContext.scope : reactive({}),
blocks: [], blocks: [],
effects: [], effects: [],
@ -251,15 +249,13 @@ export class Block {
if (opts.component) { if (opts.component) {
current.componentBlock = this; current.componentBlock = this;
this.element = stringToElement(opts.component.template); 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 { } else {
if (this.isFragment) { this.element = opts.element.cloneNode(true) as Element;
this.element = (opts.element as HTMLTemplateElement).content.cloneNode(true) as Element; opts.element.replaceWith(this.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);
}
} }
if (opts.isRoot) { if (opts.isRoot) {
@ -326,10 +322,8 @@ export class Block {
if (opts.element instanceof HTMLElement) { if (opts.element instanceof HTMLElement) {
opts.element.replaceWith(this.element); opts.element.replaceWith(this.element);
} }
} else { } else if (opts.element instanceof HTMLElement) {
if (opts.element instanceof HTMLElement) { opts.element.replaceChildren(this.element);
opts.element.replaceChildren(this.element);
}
} }
} }
} }
@ -432,128 +426,126 @@ function walk(node: Node, context: Context) {
return; return;
} }
if (isElement(node)) { if (!isElement(node)) { return; }
let exp: string | null;
const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => { let exp: string | null;
if (warnInvalidDirectives(node, [":if", ":for"])) return;
if (warnInvalidDirectives(node, [":for", ":teleport"])) return;
if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
// e.g. <div :scope="{ open: true }" /> const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => {
// In this case, the scope is merged into context.scope and will overwrite if (warnInvalidDirectives(node, [":if", ":for"])) return;
// anything returned from `main`. if (warnInvalidDirectives(node, [":for", ":teleport"])) return;
if ((exp = checkAndRemoveAttribute(node, ":scope"))) { if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
const scope = evalGet(context.scope, exp, node);
if (typeof scope === "object") { // e.g. <div :scope="{ open: true }" />
Object.assign(context.scope, scope); // In this case, the scope is merged into context.scope and will overwrite
// context = createScopedContext(context, scope); // 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; if (next) return next;
Array.from(node.attributes).forEach((attr) => { const templates = findTemplateNodes(node);
if (isPropAttribute(attr.name)) {
new AttributeDirective({ element: node, context, attr });
}
if (isEventAttribute(attr.name)) { return new Block({
new EventDirective({ element: node, context, attr }); element: node,
} app: current.componentBlock.context.app,
}); // parentContext: context,
component,
walkChildren(node, context); 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) { function walkChildren(node: Node, context: Context) {
@ -605,11 +597,7 @@ function flattenRefs(scope: any): any {
for (const key in scope) { for (const key in scope) {
if (scope.hasOwnProperty(key)) { if (scope.hasOwnProperty(key)) {
// Check if the value is a Ref // Check if the value is a Ref
if (isRef(scope[key])) { mapped[key] = isRef(scope[key]) ? scope[key].value : scope[key];
mapped[key] = scope[key].value;
} else {
mapped[key] = scope[key];
}
} }
} }
return mapped; 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>");
});
});