This commit is contained in:
nvms 2025-05-12 13:03:52 -04:00
parent 02730daae5
commit 3c07b7568d
7 changed files with 338 additions and 195 deletions

View File

@ -253,7 +253,9 @@ var EventDirective = class {
this.eventCount++; this.eventCount++;
const handler = evalGet(context.scope, attr.value, element); const handler = evalGet(context.scope, attr.value, element);
if (typeof handler === "function") { if (typeof handler === "function") {
handler(event); handler.call(context.scope, event);
} else {
execute(context.scope, this.expression, element, false);
} }
}); });
element.removeAttribute(attr.name); element.removeAttribute(attr.name);
@ -272,10 +274,10 @@ var _for = (el, exp, ctx, component, componentProps, allProps) => {
return; return;
} }
const nextNode = el.nextSibling; const nextNode = el.nextSibling;
const parent = el.parentElement; const parent2 = el.parentElement;
const anchor = new Text(""); const anchor = new Text("");
parent.insertBefore(anchor, el); parent2.insertBefore(anchor, el);
parent.removeChild(el); parent2.removeChild(el);
const sourceExp = inMatch[2].trim(); const sourceExp = inMatch[2].trim();
let valueExp = inMatch[1].trim().replace(stripParensRE, "").trim(); let valueExp = inMatch[1].trim().replace(stripParensRE, "").trim();
let destructureBindings; let destructureBindings;
@ -345,7 +347,7 @@ var _for = (el, exp, ctx, component, componentProps, allProps) => {
const mountBlock = (ctx2, ref2) => { const mountBlock = (ctx2, ref2) => {
const block = new Block({ element: el, parentContext: ctx2, replacementType: "replace", component, componentProps, allProps }); const block = new Block({ element: el, parentContext: ctx2, replacementType: "replace", component, componentProps, allProps });
block.key = ctx2.key; block.key = ctx2.key;
block.insert(parent, ref2); block.insert(parent2, ref2);
return block; return block;
}; };
ctx.effect(() => { ctx.effect(() => {
@ -378,7 +380,7 @@ var _for = (el, exp, ctx, component, componentProps, allProps) => {
if (blocks[oldIndex + 1] !== nextBlock || // If the next has moved, it must move too if (blocks[oldIndex + 1] !== nextBlock || // If the next has moved, it must move too
prevMovedBlock === nextBlock) { prevMovedBlock === nextBlock) {
prevMovedBlock = block; prevMovedBlock = block;
block.insert(parent, nextBlock ? nextBlock.element : anchor); block.insert(parent2, nextBlock ? nextBlock.element : anchor);
} }
} }
} }
@ -392,30 +394,30 @@ var _for = (el, exp, ctx, component, componentProps, allProps) => {
// src/directives/if.ts // src/directives/if.ts
function _if(el, exp, ctx, component, componentProps, allProps) { function _if(el, exp, ctx, component, componentProps, allProps) {
const parent = el.parentElement; const parent2 = el.parentElement;
const anchor = new Comment(":if"); const anchor = new Comment(":if");
parent.insertBefore(anchor, el); parent2.insertBefore(anchor, el);
const branches = [{ exp, el }]; const branches = [{ exp, el }];
let elseEl; let elseEl;
let elseExp; let elseExp;
while (elseEl = el.nextElementSibling) { while (elseEl = el.nextElementSibling) {
elseExp = null; elseExp = null;
if (checkAndRemoveAttribute(elseEl, ":else") === "" || (elseExp = checkAndRemoveAttribute(elseEl, ":else-if"))) { if (checkAndRemoveAttribute(elseEl, ":else") === "" || (elseExp = checkAndRemoveAttribute(elseEl, ":else-if"))) {
parent.removeChild(elseEl); parent2.removeChild(elseEl);
branches.push({ exp: elseExp, el: elseEl }); branches.push({ exp: elseExp, el: elseEl });
} else { } else {
break; break;
} }
} }
const nextNode = el.nextSibling; const nextNode = el.nextSibling;
parent.removeChild(el); parent2.removeChild(el);
let block; let block;
let activeBranchIndex = -1; let activeBranchIndex = -1;
const removeActiveBlock = () => { const removeActiveBlock = () => {
if (!block) { if (!block) {
return; return;
} }
parent.insertBefore(anchor, block.element); parent2.insertBefore(anchor, block.element);
block.remove(); block.remove();
block = void 0; block = void 0;
}; };
@ -426,8 +428,8 @@ function _if(el, exp, ctx, component, componentProps, allProps) {
if (i !== activeBranchIndex) { if (i !== activeBranchIndex) {
removeActiveBlock(); removeActiveBlock();
block = new Block({ element: el2, parentContext: ctx, replacementType: "replace", component, componentProps, allProps }); block = new Block({ element: el2, parentContext: ctx, replacementType: "replace", component, componentProps, allProps });
block.insert(parent, anchor); block.insert(parent2, anchor);
parent.removeChild(anchor); parent2.removeChild(anchor);
activeBranchIndex = i; activeBranchIndex = i;
} }
return; return;
@ -518,6 +520,7 @@ var ShowDirective = class {
// src/directives/teleport.ts // src/directives/teleport.ts
function _teleport(el, exp, ctx, component, componentProps, allProps) { function _teleport(el, exp, ctx, component, componentProps, allProps) {
console.log("_teleport", ctx.scope);
const anchor = new Comment(":teleport anchor"); const anchor = new Comment(":teleport anchor");
insertBefore(anchor, el); insertBefore(anchor, el);
const observed = new Comment(":teleport"); const observed = new Comment(":teleport");
@ -532,17 +535,19 @@ function _teleport(el, exp, ctx, component, componentProps, allProps) {
el.style.display = "none"; el.style.display = "none";
let block; let block;
target.appendChild(el); target.appendChild(el);
console.log("appended", el, "to", target);
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => { mutationsList.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => { mutation.removedNodes.forEach((removedNode) => {
if (removedNode.contains(observed)) { console.log("removedNode", removedNode, "observed:", observed);
if (removedNode.contains(anchor)) {
if (block.element) block.remove(); if (block.element) block.remove();
observer.disconnect(); observer.disconnect();
} }
}); });
}); });
}); });
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.querySelector(exp) || document.body, { childList: true, subtree: true });
el.style.display = originalDisplay; el.style.display = originalDisplay;
block = new Block({ block = new Block({
element: el, element: el,
@ -832,6 +837,25 @@ var link = {
}; };
// src/index.ts // src/index.ts
function provide(key, value) {
if (!current2.componentBlock) {
console.warn("Can't provide: no current component block");
}
current2.componentBlock.provides.set(key, value);
}
function inject(key) {
if (!current2.componentBlock) {
console.warn("Can't inject: no current component block");
}
let c = current2.componentBlock;
while (c) {
if (c.provides.has(key)) {
return c.provides.get(key);
}
c = c.parentComponentBlock;
}
return void 0;
}
var App2 = class { var App2 = class {
root; root;
registry = /* @__PURE__ */ new Map(); registry = /* @__PURE__ */ new Map();
@ -892,6 +916,14 @@ function createContext({ parentContext, app: app2 }) {
}; };
return context; return context;
} }
var extendScopedContext = (ctx, data = {}) => {
const parentScope = ctx.scope;
const mergedScope = { ...parentScope };
Object.defineProperties(mergedScope, Object.getOwnPropertyDescriptors(data));
const reactiveScope = reactive(mergedScope);
ctx.scope = reactiveScope;
return ctx;
};
var createScopedContext = (ctx, data = {}) => { var createScopedContext = (ctx, data = {}) => {
const parentScope = ctx.scope; const parentScope = ctx.scope;
const mergedScope = Object.create(parentScope); const mergedScope = Object.create(parentScope);
@ -970,11 +1002,24 @@ var Block = class {
} }
if (opts.component) { if (opts.component) {
this.componentProps = mergeProps(opts.componentProps ?? {}, opts.component.props ?? {}); this.componentProps = mergeProps(opts.componentProps ?? {}, opts.component.props ?? {});
if (opts.component.main) { const componentScope = opts.component.main ? opts.component.main(this.componentProps) || {} : {};
this.context.scope = { const providedKeys = /* @__PURE__ */ new Set();
...opts.component.main(this.componentProps) || {} let currentBlock = this.parentComponentBlock;
}; while (currentBlock) {
currentBlock.provides.forEach((_, key) => providedKeys.add(key));
currentBlock = currentBlock.parentComponentBlock;
} }
for (const key of providedKeys) {
if (componentScope[key] === void 0) {
const providedValue = inject(key);
if (providedValue !== void 0) {
componentScope[key] = providedValue;
}
}
}
this.context.scope = {
...componentScope
};
} }
opts.allProps?.forEach((prop) => { opts.allProps?.forEach((prop) => {
if (prop.isBind) { if (prop.isBind) {
@ -1024,14 +1069,14 @@ var Block = class {
this.componentProps[name] = value; this.componentProps[name] = value;
} }
} }
insert(parent, anchor = null) { insert(parent2, anchor = null) {
if (this.isFragment) { if (this.isFragment) {
if (this.start) { if (this.start) {
let node = this.start; let node = this.start;
let next; let next;
while (node) { while (node) {
next = node.nextSibling; next = node.nextSibling;
parent.insertBefore(node, anchor); parent2.insertBefore(node, anchor);
if (node === this.end) { if (node === this.end) {
break; break;
} }
@ -1040,12 +1085,12 @@ var Block = class {
} else { } else {
this.start = new Text(""); this.start = new Text("");
this.end = new Text(""); this.end = new Text("");
parent.insertBefore(this.end, anchor); parent2.insertBefore(this.end, anchor);
parent.insertBefore(this.start, this.end); parent2.insertBefore(this.start, this.end);
parent.insertBefore(this.element, this.end); parent2.insertBefore(this.element, this.end);
} }
} else { } else {
parent.insertBefore(this.element, anchor); parent2.insertBefore(this.element, anchor);
} }
} }
remove() { remove() {
@ -1056,12 +1101,15 @@ var Block = class {
} }
} }
if (this.start) { if (this.start) {
const parent = this.start.parentNode; const parent2 = this.start.parentNode;
if (!parent2) {
throw new Error("Parent node is null");
}
let node = this.start; let node = this.start;
let next; let next;
while (node) { while (node) {
next = node.nextSibling; next = node.nextSibling;
parent.removeChild(node); parent2.removeChild(node);
if (node === this.end) { if (node === this.end) {
break; break;
} }
@ -1076,7 +1124,7 @@ var Block = class {
this.context.blocks.forEach((block) => { this.context.blocks.forEach((block) => {
block.teardown(); block.teardown();
}); });
this.context.effects.forEach((e) => e.stop()); this.context.effects.forEach((e) => void e.stop());
} }
}; };
function isComponent(element, context) { function isComponent(element, context) {
@ -1084,7 +1132,7 @@ function isComponent(element, context) {
} }
function warnInvalidDirectives(node, directives) { function warnInvalidDirectives(node, directives) {
if (directives.every((d) => node.hasAttribute(d))) { if (directives.every((d) => node.hasAttribute(d))) {
console.warn(`These directives cannot be used together on the same node:`, directives); console.warn("These directives cannot be used together on the same node:", directives);
console.warn("Node ignored:", node); console.warn("Node ignored:", node);
return true; return true;
} }
@ -1104,12 +1152,23 @@ function walk(node, context) {
if (warnInvalidDirectives(node2, [":for", ":teleport"])) return; if (warnInvalidDirectives(node2, [":for", ":teleport"])) return;
if (warnInvalidDirectives(node2, [":if", ":teleport"])) return; if (warnInvalidDirectives(node2, [":if", ":teleport"])) return;
if (exp = checkAndRemoveAttribute(node2, ":scope")) { if (exp = checkAndRemoveAttribute(node2, ":scope")) {
const scope = evalGet(context2.scope, exp, node2);
context2.scope = extendScopedContext(context2, scope).scope;
}
if (exp = checkAndRemoveAttribute(node2, ":scope:provide")) {
const scope = evalGet(context2.scope, exp, node2); const scope = evalGet(context2.scope, exp, node2);
if (typeof scope === "object") { if (typeof scope === "object") {
Object.assign(context2.scope, scope); Object.assign(context2.scope, scope);
Object.entries(scope).forEach(([key, value]) => {
provide(key, value);
});
} }
} }
if (exp = checkAndRemoveAttribute(node2, ":if")) { if (exp = checkAndRemoveAttribute(node2, ":if")) {
if (current2.componentBlock) {
const merged = extendScopedContext(context2, current2.componentBlock?.parentComponentBlock?.context.scope);
return _if(node2, exp, merged, component, componentProps, allProps);
}
return _if(node2, exp, context2, component, componentProps, allProps); return _if(node2, exp, context2, component, componentProps, allProps);
} }
if (exp = checkAndRemoveAttribute(node2, ":for")) { if (exp = checkAndRemoveAttribute(node2, ":for")) {
@ -1175,7 +1234,7 @@ function walk(node, context) {
return new Block({ return new Block({
element: node, element: node,
app: current2.componentBlock.context.app, app: current2.componentBlock.context.app,
// parentContext: context, parentContext: context,
component, component,
replacementType: "replace", replacementType: "replace",
parentComponentBlock: current2.componentBlock, parentComponentBlock: current2.componentBlock,
@ -1197,9 +1256,9 @@ function walk(node, context) {
walkChildren(node, context); walkChildren(node, context);
} }
function walkChildren(node, context) { function walkChildren(node, context) {
let child2 = node.firstChild; let child = node.firstChild;
while (child2) { while (child) {
child2 = walk(child2, context) || child2.nextSibling; child = walk(child, context) || child.nextSibling;
} }
} }
var evalFuncCache = {}; var evalFuncCache = {};
@ -1241,34 +1300,49 @@ function flattenRefs(scope) {
} }
// src/demo.ts // src/demo.ts
var child = { var app = new App2();
var parent = {
template: html` template: html`
<div> <div>
I am child and I have a cheeseburger: "{{food}}" (does not inherit) <h1>parent, bool: {{bool}}</h1>
<div> <card>
<slot /> <!-- default -->
</div> <div :if="bool">
<div :teleport="body" id="teleported">
<div>showing 1-3 because bool is true</div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>
</div>
content 1 always shown
<div :if="bool">
content 2, animals:
<div :for="animal in animals">animal: {{animal}}</div>
</div>
<!-- body -->
<template slot="body">card body from parent</template>
</card>
</div> </div>
`, `,
main() { main() {
const food = ref("\u{1F354}"); const bool = ref(true);
return { food }; const animals = reactive(["dog", "cat", "bear"]);
setInterval(() => {
bool.value = !bool.value;
}, 2e3);
return { bool, animals };
} }
}; };
var main = { var card = {
template: html` template: html`<div>
<div class="hero sans-serif f2" :scope="{ drink: '🍹' }"> <h2>card</h2>
<div :scope="{ food: '🍕' }"> <h3><slot /></h3>
<div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div> <slot name="body" />
<child>Child slot, food: {{food}} {{drink}}</child> </div>`
</div>
</div>
`,
main() {
return { food: ref("nothing") };
}
}; };
var app = new App2(); app.register("card", card);
app.register("child", child); var parentBlock = app.mount(parent, "body");
app.mount(main, "#app"); var cardBlock = parentBlock.context.blocks[0];
//# sourceMappingURL=demo.js.map //# sourceMappingURL=demo.js.map

File diff suppressed because one or more lines are too long

View File

@ -31,53 +31,54 @@ import { html } from "./util";
// ------------------------------------------------ // ------------------------------------------------
// Slots, multiple default and named, :if and :for // Slots, multiple default and named, :if and :for
// const app = new App(); const app = new App();
// const parent = { const parent = {
// template: html` template: html`
// <div> <div>
// <h1>parent</h1> <h1>parent, bool: {{bool}}</h1>
// <card> <card>
// <!-- default --> <!-- default -->
// <div :if="bool"> <div :if="bool">
// <div :teleport="body" id="teleported"> <div :teleport="body" id="teleported">
// <div>1</div> <div>showing 1-3 because bool is true</div>
// <div>2</div> <div>1</div>
// <div>3</div> <div>2</div>
// </div> <div>3</div>
// </div> </div>
// content 1 always shown </div>
// <div :if="bool"> content 1 always shown
// content 2, animals: <div :if="bool">
// <div :for="animal in animals">animal: {{animal}}</div> content 2, animals:
// </div> <div :for="animal in animals">animal: {{animal}}</div>
</div>
// <!-- body --> <!-- body -->
// <template slot="body">card body from parent</template> <template slot="body">card body from parent</template>
// </card> </card>
// </div> </div>
// `, `,
// main() { main() {
// const bool = ref(true); const bool = ref(true);
// const animals = reactive(["dog", "cat", "bear"]); const animals = reactive(["dog", "cat", "bear"]);
// setInterval(() => { setInterval(() => {
// bool.value = !bool.value; bool.value = !bool.value;
// }, 2000); }, 2000);
// return { bool, animals }; return { bool, animals };
// }, },
// }; };
// const card = { const card = {
// template: html`<div> template: html`<div>
// <h2>card</h2> <h2>card</h2>
// <h3><slot /></h3> <h3><slot /></h3>
// <slot name="body" /> <slot name="body" />
// </div>`, </div>`,
// }; };
// app.register("card", card); app.register("card", card);
// const parentBlock = app.mount(parent, "body"); const parentBlock = app.mount(parent, "body");
// const cardBlock = parentBlock.context.blocks[0]; const cardBlock = parentBlock.context.blocks[0];
// ------------------------------------------------ // ------------------------------------------------
// Component pros, mirror and spread, bind and no bind // Component pros, mirror and spread, bind and no bind
@ -268,48 +269,50 @@ import { html } from "./util";
// const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]); // const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
// const basesReverse = computed(() => Array.from(ranks).reverse()); // 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]})` }); // const bg = (variant: string, rank: string, index: number) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
//
// return { ranks, bg }; // return { ranks, bg };
// }, // },
// }; // };
//
// const app = new App(); // const app = new App();
// app.mount(main, "#app"); // app.mount(main, "#app");
// ------------------------------------------------ // ------------------------------------------------
// :scope // :scope
const child = { // const child = {
template: html` // template: html`
<div> // <div>
I am child and I have a cheeseburger: "{{food}}" (does not inherit) // I am child and I have a cheeseburger: "{{food}}" (does not inherit)
<div> // <div>
<slot /> // <slot />
</div> // </div>
</div> // </div>
`, // `,
main() { // main() {
const food = ref("🍔"); // // Comment this out to implicitly inherit the
return { food }; // // provided :scope value from the parent.
}, // const food = ref("🍔");
}; // return { food };
// },
const main = { // };
template: html` //
<div class="hero sans-serif f2" :scope="{ drink: '🍹' }"> // const main = {
<div :scope="{ food: '🍕' }"> // template: html`
<div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div> // <div class="hero sans-serif f2" :scope:provide="{ drink: '🍹' }">
<child>Child slot, food: {{food}} {{drink}}</child> // <div :scope:provide="{ food: '🍕' }">
</div> // <div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div>
</div> // <child>Child slot, food: {{food}} {{drink}}</child>
`, // </div>
main() { // </div>
return { food: ref("nothing") }; // `,
}, // main() {
}; // // return { food: ref("nothing") };
// },
const app = new App(); // };
app.register("child", child); //
app.mount(main, "#app"); // const app = new App();
// app.register("child", child);
// app.mount(main, "#app");
// ------------------------------------------------ // ------------------------------------------------
// Practical :scope demo // Practical :scope demo
@ -318,18 +321,16 @@ app.mount(main, "#app");
// <div class="hero sans-serif f2" :scope="{ visible: true }"> // <div class="hero sans-serif f2" :scope="{ visible: true }">
// <div :show="visible">ON</div> // <div :show="visible">ON</div>
// <div :show="!visible">OFF</div> // <div :show="!visible">OFF</div>
// <button @click="() => visible = !visible" class="padding-x-1 padding-y-0_5 border-radius-1">Toggle</button> // <button @click="visible = !visible" class="padding-x-1 padding-y-0_5 border-radius-0_25">Toggle</button>
// <div>visible: {{visible}} / foo: {{foo}}</div>
// </div> // </div>
// `, // `,
// main() { // main() {
// const onClick = () => { // const foo = ref("bar");
// console.log("ok"); // return { foo };
// };
//
// return { onClick };
// }, // },
// }; // };
//
// const app = new App(); // const app = new App();
// app.mount(main, "#app"); // app.mount(main, "#app");

View File

@ -1,4 +1,4 @@
import { Context, evalGet } from ".."; import { Context, evalGet, execute } from "..";
interface EventDirectiveOptions { interface EventDirectiveOptions {
element: Element; element: Element;
@ -31,7 +31,9 @@ export class EventDirective {
const handler = evalGet(context.scope, attr.value, element); const handler = evalGet(context.scope, attr.value, element);
if (typeof handler === "function") { if (typeof handler === "function") {
handler(event); handler.call(context.scope, event);
} else {
execute(context.scope, this.expression, element, false);
} }
}); });

View File

@ -7,6 +7,7 @@ interface Branch {
} }
export function _if(el: Element, exp: string, ctx: Context, component?: Component, componentProps?: Record<string, any>, allProps?: Record<string, any>) { export function _if(el: Element, exp: string, ctx: Context, component?: Component, componentProps?: Record<string, any>, allProps?: Record<string, any>) {
// console.log("_if received scope", ctx.scope);
const parent = el.parentElement!; const parent = el.parentElement!;
const anchor = new Comment(":if"); const anchor = new Comment(":if");
@ -45,6 +46,7 @@ export function _if(el: Element, exp: string, ctx: Context, component?: Componen
ctx.effect(() => { ctx.effect(() => {
for (let i = 0; i < branches.length; i++) { for (let i = 0; i < branches.length; i++) {
const { exp, el } = branches[i]; const { exp, el } = branches[i];
// console.log("scope", ctx.scope);
if (!exp || evalGet(ctx.scope, exp, el)) { if (!exp || evalGet(ctx.scope, exp, el)) {
if (i !== activeBranchIndex) { if (i !== activeBranchIndex) {

View File

@ -1,7 +1,8 @@
import { Block, Component, Context } from ".."; import { Block, Component, Context, evalGet } from "..";
import { insertBefore } from "../util"; import { insertBefore, nextTick } from "../util";
export function _teleport(el: Element, exp: string, ctx: Context, component?: Component, componentProps?: Record<string, any>, allProps?: Record<string, any>) { export function _teleport(el: Element, exp: string, ctx: Context, component?: Component, componentProps?: Record<string, any>, allProps?: Record<string, any>) {
console.log("_teleport", ctx.scope);
const anchor = new Comment(":teleport anchor"); const anchor = new Comment(":teleport anchor");
insertBefore(anchor, el); insertBefore(anchor, el);
const observed = new Comment(":teleport"); const observed = new Comment(":teleport");
@ -23,12 +24,19 @@ export function _teleport(el: Element, exp: string, ctx: Context, component?: Co
let block: Block; let block: Block;
target.appendChild(el); // nextTick(() => {
target.appendChild(el);
console.log("appended", el, "to", target);
// })
// setTimeout(() => {
// });
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => { mutationsList.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => { mutation.removedNodes.forEach((removedNode) => {
if (removedNode.contains(observed)) { console.log("removedNode", removedNode, "observed:", observed);
// if (removedNode.contains(observed)) {
if (removedNode.contains(anchor)) {
if (block.element) block.remove(); if (block.element) block.remove();
observer.disconnect(); observer.disconnect();
} }
@ -36,7 +44,8 @@ export function _teleport(el: Element, exp: string, ctx: Context, component?: Co
}); });
}); });
observer.observe(document.body, { childList: true, subtree: true }); // observer.observe(document.body, { childList: true, subtree: true });
observer.observe(document.querySelector(exp) || document.body, { childList: true, subtree: true });
// @ts-ignore // @ts-ignore
el.style.display = originalDisplay; el.style.display = originalDisplay;

View File

@ -6,30 +6,12 @@ import { InterpolationDirective } from "./directives/interpolation";
import { ShowDirective } from "./directives/show"; import { ShowDirective } from "./directives/show";
import { _teleport } from "./directives/teleport"; import { _teleport } from "./directives/teleport";
import { ValueDirective } from "./directives/value"; import { ValueDirective } from "./directives/value";
import { Plugin } from "./plugins"; import type { Plugin } from "./plugins";
import { isComputed } from "./reactivity/computed"; import { isComputed } from "./reactivity/computed";
import { effect as _effect } from "./reactivity/effect"; import { effect as _effect } from "./reactivity/effect";
import { reactive } from "./reactivity/reactive"; import { reactive } from "./reactivity/reactive";
import { isRef } from "./reactivity/ref"; import { isRef } from "./reactivity/ref";
import { import { checkAndRemoveAttribute, componentHasPropByName, extractPropName, findSlotNodes, findTemplateNodes, isElement, isEventAttribute, isMirrorProp, isObject, isPropAttribute, isRegularProp, isSpreadProp, isText, type Slot, stringToElement, type Template, toDisplayString } from "./util";
checkAndRemoveAttribute,
componentHasPropByName,
extractPropName,
findSlotNodes,
findTemplateNodes,
isElement,
isEventAttribute,
isMirrorProp,
isObject,
isPropAttribute,
isRegularProp,
isSpreadProp,
isText,
Slot,
stringToElement,
Template,
toDisplayString,
} from "./util";
export * from "./plugins"; export * from "./plugins";
export * from "./plugins/router"; export * from "./plugins/router";
@ -145,9 +127,29 @@ export function createContext({ parentContext, app }: CreateContextOptions): Con
}, },
}; };
// console.log('createContext, scope:', context.scope);
return context; return context;
} }
export const extendScopedContext = (ctx: Context, data = {}): Context => {
const parentScope = ctx.scope;
// Create a new object that inherits from parentScope
const mergedScope = { ...parentScope };
// Add the new properties from data
Object.defineProperties(mergedScope, Object.getOwnPropertyDescriptors(data));
// Make it reactive
const reactiveScope = reactive(mergedScope);
// Assign it back to the context
ctx.scope = reactiveScope;
return ctx;
};
export const createScopedContext = (ctx: Context, data = {}): Context => { export const createScopedContext = (ctx: Context, data = {}): Context => {
const parentScope = ctx.scope; const parentScope = ctx.scope;
const mergedScope = Object.create(parentScope); const mergedScope = Object.create(parentScope);
@ -232,15 +234,15 @@ export class Block {
context: Context; context: Context;
parentContext: Context; parentContext: Context;
component: Component; component: Component;
provides = new Map<string, any>(); provides = new Map<string, unknown>();
parentComponentBlock: Block | undefined; parentComponentBlock: Block | undefined;
componentProps: Record<string, any>; componentProps: Record<string, unknown>;
allProps: Record<string, any>; allProps: Record<string, unknown>;
isFragment: boolean; isFragment: boolean;
start?: Text; start?: Text;
end?: Text; end?: Text;
key?: any; key?: unknown;
constructor(opts: BlockOptions) { constructor(opts: BlockOptions) {
this.isFragment = opts.element instanceof HTMLTemplateElement; this.isFragment = opts.element instanceof HTMLTemplateElement;
@ -265,16 +267,39 @@ export class Block {
this.parentContext = opts.parentContext ? opts.parentContext : createContext({}); this.parentContext = opts.parentContext ? opts.parentContext : createContext({});
this.parentContext.blocks.push(this); this.parentContext.blocks.push(this);
this.context = createContext({ parentContext: opts.parentContext, app: opts.app }); this.context = createContext({ parentContext: opts.parentContext, app: opts.app });
// console.log('created context', this.context.scope);
// console.log('opts.parentContext was', opts.parentContext.scope);
} }
if (opts.component) { if (opts.component) {
this.componentProps = mergeProps(opts.componentProps ?? {}, opts.component.props ?? {}); this.componentProps = mergeProps(opts.componentProps ?? {}, opts.component.props ?? {});
if (opts.component.main) { // Create the component scope
this.context.scope = { const componentScope = opts.component.main ? opts.component.main(this.componentProps) || {} : {};
...(opts.component.main(this.componentProps) || {}),
}; // Check for provided values from parent components
// First, get all provided keys from parent components
const providedKeys = new Set<string>();
let currentBlock = this.parentComponentBlock;
while (currentBlock) {
currentBlock.provides.forEach((_, key) => providedKeys.add(key));
currentBlock = currentBlock.parentComponentBlock;
} }
// Then inject values for each provided key
for (const key of providedKeys) {
// Only inject if the component doesn't already define this value
if (componentScope[key] === undefined) {
const providedValue = inject(key);
if (providedValue !== undefined) {
componentScope[key] = providedValue;
}
}
}
this.context.scope = {
...componentScope,
};
} }
opts.allProps?.forEach((prop: any) => { opts.allProps?.forEach((prop: any) => {
@ -376,7 +401,10 @@ export class Block {
} }
if (this.start) { if (this.start) {
const parent = this.start.parentNode!; const parent = this.start.parentNode;
if (!parent) {
throw new Error("Parent node is null");
}
let node: Node | null = this.start; let node: Node | null = this.start;
let next: Node | null; let next: Node | null;
@ -402,7 +430,7 @@ export class Block {
block.teardown(); block.teardown();
}); });
this.context.effects.forEach((e) => e.stop()); this.context.effects.forEach((e) => void e.stop());
} }
} }
@ -412,7 +440,7 @@ function isComponent(element: Element, context: Context) {
function warnInvalidDirectives(node: Element, directives: string[]): boolean { function warnInvalidDirectives(node: Element, directives: string[]): boolean {
if (directives.every((d) => node.hasAttribute(d))) { if (directives.every((d) => node.hasAttribute(d))) {
console.warn(`These directives cannot be used together on the same node:`, directives); console.warn("These directives cannot be used together on the same node:", directives);
console.warn("Node ignored:", node); console.warn("Node ignored:", node);
return true; return true;
} }
@ -426,7 +454,9 @@ function walk(node: Node, context: Context) {
return; return;
} }
if (!isElement(node)) { return; } if (!isElement(node)) {
return;
}
let exp: string | null; let exp: string | null;
@ -438,15 +468,39 @@ function walk(node: Node, context: Context) {
// e.g. <div :scope="{ open: true }" /> // e.g. <div :scope="{ open: true }" />
// In this case, the scope is merged into context.scope and will overwrite // In this case, the scope is merged into context.scope and will overwrite
// anything returned from `main`. // 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, ":scope"))) { if ((exp = checkAndRemoveAttribute(node, ":scope"))) {
const scope = evalGet(context.scope, exp, node);
context.scope = extendScopedContext(context, scope).scope;
}
// Handle :scope:provide directive - provides values to all descendants
if ((exp = checkAndRemoveAttribute(node, ":scope:provide"))) {
const scope = evalGet(context.scope, exp, node); const scope = evalGet(context.scope, exp, node);
if (typeof scope === "object") { if (typeof scope === "object") {
// First, merge into the current scope
Object.assign(context.scope, scope); Object.assign(context.scope, scope);
// context = createScopedContext(context, scope);
// Then provide each key-value pair to descendants
Object.entries(scope).forEach(([key, value]) => {
provide(key, value);
});
} }
} }
if ((exp = checkAndRemoveAttribute(node, ":if"))) { if ((exp = checkAndRemoveAttribute(node, ":if"))) {
// TODO: Uh
if (current.componentBlock) {
const merged = extendScopedContext(context, current.componentBlock?.parentComponentBlock?.context.scope);
return _if(node, exp, merged, component, componentProps, allProps);
}
return _if(node, exp, context, component, componentProps, allProps); return _if(node, exp, context, component, componentProps, allProps);
} }
if ((exp = checkAndRemoveAttribute(node, ":for"))) { if ((exp = checkAndRemoveAttribute(node, ":for"))) {
@ -486,17 +540,18 @@ function walk(node: Node, context: Context) {
} }
}; };
const processAttributes = (node: Element, component?: Component) => Array.from(node.attributes) const processAttributes = (node: Element, component?: Component) =>
.filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || (isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component))) Array.from(node.attributes)
.map((attr) => ({ .filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || (isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)))
isMirror: isMirrorProp(attr.name), .map((attr) => ({
isSpread: isSpreadProp(attr.name), isMirror: isMirrorProp(attr.name),
isBind: attr.name.includes("bind"), isSpread: isSpreadProp(attr.name),
originalName: attr.name, isBind: attr.name.includes("bind"),
extractedName: extractPropName(attr.name), originalName: attr.name,
exp: attr.value, extractedName: extractPropName(attr.name),
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node) : attr.value ? evalGet(context.scope, attr.value, node) : undefined, 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)) { if (isComponent(node, context)) {
const component = context.app.getComponent(node.tagName.toLowerCase()); const component = context.app.getComponent(node.tagName.toLowerCase());
@ -522,7 +577,7 @@ function walk(node: Node, context: Context) {
return new Block({ return new Block({
element: node, element: node,
app: current.componentBlock.context.app, app: current.componentBlock.context.app,
// parentContext: context, parentContext: context,
component, component,
replacementType: "replace", replacementType: "replace",
parentComponentBlock: current.componentBlock, parentComponentBlock: current.componentBlock,
@ -568,7 +623,7 @@ export function evalSet(scope: any, exp: string, value: unknown) {
return execute(scope, `const ___target = (${exp.trim()}); return $isRef(___target) ? ___target.value = ${value} : ___target = ${value};`, null, false); return execute(scope, `const ___target = (${exp.trim()}); return $isRef(___target) ? ___target.value = ${value} : ___target = ${value};`, null, false);
} }
function execute(scope: any, exp: string, el?: Node, flatRefs = true) { export function execute(scope: any, exp: string, el?: Node, flatRefs = true) {
const newScope = flatRefs ? flattenRefs(scope) : scope; const newScope = flatRefs ? flattenRefs(scope) : scope;
const fn = evalFuncCache[exp] || (evalFuncCache[exp] = toFunction(exp)); const fn = evalFuncCache[exp] || (evalFuncCache[exp] = toFunction(exp));