This commit is contained in:
nvms 2024-10-20 16:29:39 -04:00
parent 0019cd64e5
commit 0d2a1b6e82
12 changed files with 1663 additions and 1571 deletions

View File

@ -1,5 +1,5 @@
{
"printWidth": 200,
"printWidth": 300,
"semi": true,
"singleQuote": false,
"tabWidth": 2

View File

@ -2,7 +2,7 @@
"name": "soma3",
"version": "0.0.1",
"scripts": {
"serve": "esr --serve src/index.ts",
"serve": "esr --serve src/demo.ts",
"build": "esr --build src/index.ts",
"build:watch": "esr --build --watch src/index.ts",
"run": "esr --run src/index.ts",

1285
public/demo.js Executable file

File diff suppressed because it is too large Load Diff

7
public/demo.js.map Executable file

File diff suppressed because one or more lines are too long

1
public/index.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,23 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<link rel="stylesheet" href="index.css" />
<style>
:root {
--light-start: 0.92;
--light-end: 0.13;
--chroma-start: 0.001;
--chroma-end: 0.01;
}
</style>
<title>Application</title>
{{ css }}
</head>
<body>
<div id="app"></div>
{{ livereload }}
{{ js }}
{{ livereload }} {{ js }}
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

299
src/demo.ts Normal file
View File

@ -0,0 +1,299 @@
import { App } from ".";
import { ref } from "./reactivity/ref";
import { html } from "./util";
// ------------------------------------------------
// Slots, multiple default and named, :if and :for
// const card = {
// template: html`<div>
// <h3><slot /></h3>
// <slot name="body" />
// </div>`,
// };
// const main = {
// template: html`
// <div>
// <h1>card below</h1>
// <card>
// card title
// <template slot="body">Card body content</template>
// </card>
// </div>
// `,
// };
// const app = new App();
// app.register("card", card);
// app.mount(main, "#app");
// ------------------------------------------------
// Slots, multiple default and named, :if and :for
// const app = new App();
// const parent = {
// template: html`
// <div>
// <h1>parent</h1>
// <card>
// <!-- default -->
// <div :if="bool">
// <div :teleport="body" id="teleported">
// <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>
// `,
// main() {
// const bool = ref(true);
// const animals = reactive(["dog", "cat", "bear"]);
// setInterval(() => {
// bool.value = !bool.value;
// }, 2000);
// return { bool, animals };
// },
// };
// const card = {
// template: html`<div>
// <h2>card</h2>
// <h3><slot /></h3>
// <slot name="body" />
// </div>`,
// };
// app.register("card", card);
// const parentBlock = app.mount(parent, "body");
// const cardBlock = parentBlock.context.blocks[0];
// ------------------------------------------------
// Component pros, mirror and spread, bind and no bind
// const child = {
// template: html`<div>Animal: {{animal}} {{index}}</div>`,
// props: { animal: { default: "cat" }, index: { default: 0 } },
// main({ animal, index }) {
// return { animal, index };
// },
// };
// const parent = {
// template: html`
// <div class="sans-serif">
// <child :if="true" :for="x in list" />
// mirror, no bind:
// <child {animal} />
// <hr />
// mirror, bind:
// <child {animal:bind} />
// <hr />
// spread, no bind:
// <child ...spread />
// <hr />
// spread, bind:
// <child ...spread:bind />
// <hr />
// regular prop:
// <child .animal="animal" />
// <hr />
// regular prop, bind:
// <child .animal:bind="animal" />
// <hr />
// <div .id="animal">div has "id" set to animal.value</div>
// <hr />
// <div .id:bind="animal">div has "id" set and bound to animal.value</div>
// <hr />
// <div {animal}>div has "animal" set to animal.value</div>
// <hr />
// <div {animal:bind}>div has "animal" set and bound to animal.value</div>
// <hr />
// <div ...spread>div has "animal" spread</div>
// <hr />
// <div ...spread:bind>div has "animal" spread and bound</div>
// <hr />
// <hr />
// <hr />
// <hr />
// if bool, mirror, no bind:
// <child :if="bool" {animal} />
// if bool, mirror, bind:
// <child :if="bool" {animal:bind} />
// <hr />
// for list, mirror, no bind:
// <child :for="item in list" {animal} />
// <hr />
// for list, mirror, bind:
// <child :for="item in list" {animal:bind} />
// if bool, for list, mirror, no bind: these have the value "DOG!" because by the time for :for directive is evaluated, animal.value is "DOG!", and no longer "dog".
// <div :if="bool">
// <child :for="item in list" {animal} />
// </div>
// </div>
// `,
// main() {
// const bool = ref(false);
// const animal = ref("dog");
// const spread = reactive({ animal: "panther" });
// const list = reactive([1, 2, 3]);
// setTimeout(() => {
// spread.animal = "PANTHER!";
// animal.value = "DOG!";
// bool.value = true;
// }, 500);
// setTimeout(() => {
// animal.value = "DOG!!!!!";
// }, 1000);
// return { animal, spread, bool, list };
// },
// };
// const app = new App();
// app.register("child", child);
// app.mount(parent, "#app");
// ------------------------------------------------
// Event directive
// const counter = {
// template: html`
// <div>
// <div :teleport="body" :if="style.color === 'gray'">true</div>
// <h3 {style:bind}>Count: {{count}}{{count >= 2 ? '!!!' : ''}}</h3>
// <button @click="increment">+</button>
// <button @click="decrement">-</button>
// </div>
// `,
// main() {
// const count = ref(0);
// 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");
// ------------------------------------------------
// Template
// const main = {
// template: html`
// <div>
// <div :if="bool">
// <div :for="item in items">{{item}}</div>
// </div>
// </div>
// `,
// main() {
// const items = reactive([1, 2, 3, 4, 5]);
// const bool = ref(true);
// setInterval(() => (bool.value = !bool.value), 250);
// return { items, bool };
// },
// };
// const app = new App();
// app.mount(main, "#app");
// ------------------------------------------------
// :html
// const main = {
// template: html`<div :html="html"></div>`,
// main() {
// const html = ref("<h1>hello</h1>");
// setTimeout(() => {
// if (html.value === "<h1>hello</h1>") {
// html.value = "<h1>world</h1>";
// }
// }, 1000);
// return { html };
// },
// };
// const app = new App();
// app.mount(main, "#app");
// ------------------------------------------------
// 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-5">Colors</h1>
// <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>
hello from child, 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">
<div :scope="{ food: '🍕' }">
<!-- <div> -->
<div>Scoped data: {{food}}</div>
<child>Child slot, food: {{food}}</child>
<div :if="food === 'nothing'">No pizza 😢</div>
<div :else-if="food === '🍕'">Pizza!</div>
</div>
</div>
`,
main() {
return { food: ref("nothing") };
},
};
const app = new App();
app.register("child", child);
app.mount(main, "#app");

View File

@ -88,6 +88,7 @@ export class AttributeDirective {
this.element.classList.remove(c);
});
} else if (typeof value === "object" && this.extractedAttributeName === "style") {
console.log("value is object", value)
const next = Object.keys(value);
const rm = Object.keys(this.previousStyles).filter((style) => !next.includes(style));

View File

@ -10,26 +10,11 @@ import { Plugin } from "./plugins";
import { isComputed } from "./reactivity/computed";
import { effect as _effect } from "./reactivity/effect";
import { reactive } from "./reactivity/reactive";
import { isRef, ref } from "./reactivity/ref";
import {
checkAndRemoveAttribute,
componentHasPropByName,
extractPropName,
findSlotNodes,
findTemplateNodes,
html,
isElement,
isEventAttribute,
isMirrorProp,
isObject,
isPropAttribute,
isRegularProp,
isSpreadProp,
isText,
Slot,
stringToElement,
Template,
} from "./util";
import { isRef } from "./reactivity/ref";
import { 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/router";
export function provide(key: string, value: unknown) {
if (!current.componentBlock) {
@ -131,7 +116,8 @@ interface CreateContextOptions {
export function createContext({ parentContext, app }: CreateContextOptions): Context {
const context: Context = {
app: app ? app : parentContext && parentContext.app ? parentContext.app : null,
scope: parentContext ? parentContext.scope : reactive({}),
// scope: parentContext ? parentContext.scope : reactive({}),
scope: reactive({}),
blocks: [],
effects: [],
slots: [],
@ -408,6 +394,16 @@ function isComponent(element: Element, context: Context) {
return !!context.app.getComponent(element.tagName.toLowerCase());
}
function warnInvalidDirectives(node: Element, directives: string[]): boolean {
if (directives.every((d) => node.hasAttribute(d))) {
console.warn(`These directives cannot be used together on the same node:`, directives);
console.warn("Node ignored:", node);
return true;
}
return false;
}
function walk(node: Node, context: Context) {
if (isText(node)) {
new InterpolationDirective({ element: node, context });
@ -418,6 +414,21 @@ function walk(node: Node, context: Context) {
let exp: string | null;
const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => {
if (warnInvalidDirectives(node, [":if", ":for"])) return;
if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
if (warnInvalidDirectives(node, [":for", ":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);
if (typeof scope === "object") {
Object.assign(context.scope, scope);
// context = createScopedContext(context, scope);
}
}
if ((exp = checkAndRemoveAttribute(node, ":teleport"))) {
return _teleport(node, exp, context);
}
@ -447,6 +458,11 @@ function walk(node: Node, context: Context) {
}
});
}
if ((exp = checkAndRemoveAttribute(node, ":text"))) {
context.effect(() => {
node.textContent = toDisplayString(evalGet(context.scope, exp));
});
}
};
const processAttributes = (node: Element, component?: Component) => {
@ -571,233 +587,3 @@ function flattenRefs(scope: any): any {
}
return mapped;
}
// ------------------------------------------------
// Slots, multiple default and named, :if and :for
// const card = {
// template: html`<div>
// <h3><slot /></h3>
// <slot name="body" />
// </div>`,
// };
// const main = {
// template: html`
// <div>
// <h1>card below</h1>
// <card>
// card title
// <template slot="body">Card body content</template>
// </card>
// </div>
// `,
// };
// const app = new App();
// app.register("card", card);
// app.mount(main, "#app");
// ------------------------------------------------
// Slots, multiple default and named, :if and :for
// const app = new App();
// const parent = {
// template: html`
// <div>
// <h1>parent</h1>
// <card>
// <!-- default -->
// <div :if="bool">
// <div :teleport="body" id="teleported">
// <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>
// `,
// main() {
// const bool = ref(true);
// const animals = reactive(["dog", "cat", "bear"]);
// setInterval(() => {
// bool.value = !bool.value;
// }, 2000);
// return { bool, animals };
// },
// };
// const card = {
// template: html`<div>
// <h2>card</h2>
// <h3><slot /></h3>
// <slot name="body" />
// </div>`,
// };
// app.register("card", card);
// const parentBlock = app.mount(parent, "body");
// const cardBlock = parentBlock.context.blocks[0];
// ------------------------------------------------
// Component pros, mirror and spread, bind and no bind
const child = {
template: html`<div>Animal: {{animal}} {{index}}</div>`,
props: { animal: { default: "cat" }, index: { default: 0 } },
main({ animal, index }) {
return { animal, index };
},
};
const parent = {
template: html`
<div>
mirror, no bind:
<child {animal} />
<hr />
mirror, bind:
<child {animal:bind} />
<hr />
spread, no bind:
<child ...spread />
<hr />
spread, bind:
<child ...spread:bind />
<hr />
regular prop:
<child .animal="animal" />
<hr />
regular prop, bind:
<child .animal:bind="animal" />
<hr />
<div .id="animal">div has "id" set to animal.value</div>
<hr />
<div .id:bind="animal">div has "id" set and bound to animal.value</div>
<hr />
<div {animal}>div has "animal" set to animal.value</div>
<hr />
<div {animal:bind}>div has "animal" set and bound to animal.value</div>
<hr />
<div ...spread>div has "animal" spread</div>
<hr />
<div ...spread:bind>div has "animal" spread and bound</div>
<hr />
<hr />
<hr />
<hr />
if bool, mirror, no bind:
<child :if="bool" {animal} />
if bool, mirror, bind:
<child :if="bool" {animal:bind} />
<hr />
for list, mirror, no bind:
<child :for="item in list" {animal} />
<hr />
for list, mirror, bind:
<child :for="item in list" {animal:bind} />
if bool, for list, mirror, no bind: these have the value "DOG!" because by the time for :for directive is evaluated, animal.value is "DOG!", and no longer "dog".
<div :if="bool">
<child :for="item in list" {animal} />
</div>
</div>
`,
main() {
const bool = ref(false);
const animal = ref("dog");
const spread = reactive({ animal: "panther" });
const list = reactive([1, 2, 3]);
setTimeout(() => {
spread.animal = "PANTHER!";
animal.value = "DOG!";
bool.value = true;
}, 500);
setTimeout(() => {
animal.value = "DOG!!!!!";
}, 1000);
return { animal, spread, bool, list };
},
};
const app = new App();
app.register("child", child);
app.mount(parent, "#app");
// ------------------------------------------------
// Event directive
// const counter = {
// template: html`
// <div>
// <div :teleport="body" :if="style.color === 'gray'">true</div>
// <h3 {style:bind}>Count: {{count}}{{count >= 2 ? '!!!' : ''}}</h3>
// <button @click="increment">+</button>
// <button @click="decrement">-</button>
// </div>
// `,
// main() {
// const count = ref(0);
// 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");
// ------------------------------------------------
// Template
// const main = {
// template: html`
// <div>
// <div :if="bool">
// <div :for="item in items">{{item}}</div>
// </div>
// </div>
// `,
// main() {
// const items = reactive([1, 2, 3, 4, 5]);
// const bool = ref(true);
// setInterval(() => (bool.value = !bool.value), 250);
// return { items, bool };
// },
// };
// const app = new App();
// app.mount(main, "#app");
// ------------------------------------------------
// :html
// const main = {
// template: html`<div :html="html"></div>`,
// main() {
// const html = ref("<h1>hello</h1>");
// setTimeout(() => {
// if (html.value === "<h1>hello</h1>") {
// html.value = "<h1>world</h1>";
// }
// }, 1000);
// return { html };
// },
// };
// const app = new App();
// app.mount(main, "#app");

View File

@ -23,3 +23,5 @@ export type RouteMatch = {
match: RouteExpression;
parents: Route[];
};
export { RouterPlugin } from "./plugin";