This commit is contained in:
nvms 2024-10-21 10:10:08 -04:00
parent 4e18b46c89
commit bceca3053c
9 changed files with 1953 additions and 143 deletions

View File

@ -68,16 +68,6 @@ function findTemplateNodes(element) {
findTemplates(element);
return templates;
}
var nextTick = async (f) => {
await new Promise(
(r) => setTimeout(
(_) => requestAnimationFrame((_2) => {
f && f();
r();
})
)
);
};
function html(strings, ...values) {
const selfClosingTags = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
let result = strings.reduce((acc, str, i) => acc + str + (values[i] || ""), "");
@ -99,6 +89,9 @@ function insertAfter(newNode, existingNode) {
existingNode?.parentNode?.appendChild(newNode);
}
}
function insertBefore(newNode, existingNode) {
existingNode.parentNode?.insertBefore(newNode, existingNode);
}
function isPropAttribute(attrName) {
if (attrName.startsWith(".")) {
return true;
@ -222,7 +215,6 @@ var AttributeDirective = class {
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));
next.forEach((style) => {
@ -524,31 +516,40 @@ var ShowDirective = class {
};
// src/directives/teleport.ts
function _teleport(el, exp, ctx) {
const anchor = new Comment(":teleport");
el.replaceWith(anchor);
function _teleport(el, exp, ctx, component, componentProps, allProps) {
const anchor = new Comment(":teleport anchor");
insertBefore(anchor, el);
const observed = new Comment(":teleport");
el.replaceWith(observed);
console.log("Creating new block with allProps", component);
const target = document.querySelector(exp);
if (!target) {
console.warn(`teleport target not found: ${exp}`);
return;
}
nextTick(() => {
target.appendChild(el);
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => {
if (removedNode.contains(anchor)) {
el.remove();
observer.disconnect();
}
});
const originalDisplay = el.style.display;
el.style.display = "none";
let block;
target.appendChild(el);
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => {
if (removedNode.contains(observed)) {
if (block.element) block.remove();
observer.disconnect();
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
new Block({
element: el,
parentContext: ctx
});
});
observer.observe(document.body, { childList: true, subtree: true });
el.style.display = originalDisplay;
block = new Block({
element: el,
parentContext: ctx,
replacementType: "replace",
component,
componentProps,
allProps
});
return anchor;
}
@ -729,6 +730,18 @@ 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");
@ -848,7 +861,7 @@ var App2 = class {
const root = typeof target === "string" ? document.querySelector(target) : target;
const display = root.style.display;
root.style.display = "none";
this.root = this._mount(component, root, props);
this._mount(component, root, props);
root.style.display = display;
return this.root;
}
@ -861,6 +874,7 @@ var App2 = class {
parentContext.scope.$isRef = isRef;
parentContext.scope.$isComputed = isComputed;
const block = new Block({
app: this,
element: target,
parentContext,
component,
@ -877,8 +891,7 @@ var App2 = class {
function createContext({ parentContext, app: app2 }) {
const context = {
app: app2 ? app2 : parentContext && parentContext.app ? parentContext.app : null,
// scope: parentContext ? parentContext.scope : reactive({}),
scope: reactive({}),
scope: parentContext ? parentContext.scope : reactive({}),
blocks: [],
effects: [],
slots: [],
@ -963,10 +976,11 @@ var Block = class {
}
if (opts.isRoot) {
this.context = opts.parentContext;
opts.app.root = this;
} else {
this.parentContext = opts.parentContext ? opts.parentContext : createContext({});
this.parentContext.blocks.push(this);
this.context = createContext({ parentContext: opts.parentContext });
this.context = createContext({ parentContext: opts.parentContext, app: opts.app });
}
if (opts.component) {
this.componentProps = mergeProps(opts.componentProps ?? {}, opts.component.props ?? {});
@ -1101,23 +1115,23 @@ function walk(node, context) {
let exp;
const handleDirectives = (node2, context2, component, componentProps, allProps) => {
if (warnInvalidDirectives(node2, [":if", ":for"])) return;
if (warnInvalidDirectives(node2, [":if", ":teleport"])) 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, ":teleport")) {
return _teleport(node2, exp, context2);
}
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 });
}
@ -1128,8 +1142,9 @@ function walk(node, context) {
new ValueDirective({ element: node2, context: context2, expression: exp });
}
if (exp = checkAndRemoveAttribute(node2, ":html")) {
const htmlExp = exp;
context2.effect(() => {
const result = evalGet(context2.scope, exp, node2);
const result = evalGet(context2.scope, htmlExp, node2);
if (result instanceof Element) {
node2.replaceChildren();
node2.append(result);
@ -1139,8 +1154,9 @@ function walk(node, context) {
});
}
if (exp = checkAndRemoveAttribute(node2, ":text")) {
const textExp = exp;
context2.effect(() => {
node2.textContent = toDisplayString(evalGet(context2.scope, exp, node2));
node2.textContent = toDisplayString(evalGet(context2.scope, textExp, node2));
});
}
};
@ -1174,7 +1190,8 @@ function walk(node, context) {
const templates = findTemplateNodes(node);
return new Block({
element: node,
parentContext: context,
app: current.componentBlock.context.app,
// parentContext: context,
component,
replacementType: "replace",
parentComponentBlock: current.componentBlock,
@ -1247,17 +1264,22 @@ function flattenRefs(scope) {
// src/demo.ts
var main = {
template: html`
<div class="hero sans-serif f2" :scope="{ visible: true }">
<div :show="visible">ON</div>
<div :show="!visible">OFF</div>
<button @click="() => visible = !visible" class="padding-x-1 padding-y-0_5 border-radius-1">Toggle</button>
<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 onClick = () => {
console.log("ok");
};
return { onClick };
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 };
}
};
var app = new App2();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import { App } from ".";
import { computed } from "./reactivity/computed";
import { reactive } from "./reactivity/reactive";
import { ref } from "./reactivity/ref";
import { html } from "./util";
@ -169,11 +170,16 @@ import { html } from "./util";
// Event directive
// const counter = {
// template: html`
// <div>
// <div :teleport="body" :if="style.color === 'gray'">true</div>
// <div class="sans-serif">
// <div :teleport="body">
// <div :if="style.color === 'gray'">true</div>
// </div>
// <h3 {style:bind}>Count: {{count}}{{count >= 2 ? '!!!' : ''}}</h3>
// <button @click="increment">+</button>
// <button @click="decrement">-</button>
// <button @click="increment" class="padding-x-1 padding-y-0_5">+</button>
// <button @click="decrement" class="padding-x-1 padding-y-0_5">-</button>
// <div :teleport="body">
// <div :for="color in ['red', 'green', 'blue']">{{color}}</div>
// </div>
// </div>
// `,
// main() {
@ -194,12 +200,18 @@ import { html } from "./util";
// app.mount(counter, "#app");
// ------------------------------------------------
// Template
// :if above :for
// :if with :teleport
// const main = {
// template: html`
// <div>
// <div :if="bool">
// <div :for="item in items">{{item}}</div>
// <div :teleport="body">
// <div :for="item in items">{{item}}</div>
// </div>
// </div>
// <div :if="bool">
// <div :teleport="body">if bool teleported! {{items}}</div>
// </div>
// </div>
// `,
@ -236,37 +248,37 @@ 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-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]})` });
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 };
// },
// };
return { ranks, bg };
},
};
// const app = new App();
// app.mount(main, "#app");
const app = new App();
app.mount(main, "#app");
// ------------------------------------------------
// :scope
// const child = {
// template: html`
// <div>
// hello from child, food: "{{food}}" (does not inherit)
// I am child and I have food: "{{food}}" (does not inherit)
// <div>
// <slot />
// </div>
@ -277,16 +289,13 @@ import { html } from "./util";
// return { food };
// },
// };
//
// const main = {
// template: html`
// <div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
// <div :scope="{ food: '🍕' }">
// <!-- <div> -->
// <div>Scoped data: {{food}}</div>
// <div>Scoped food: {{food}} and scoped drink: {{drink}}</div>
// <child>Child slot, food: {{food}} {{drink}}</child>
// <div :if="food === 'nothing'">No pizza 😢</div>
// <div :else-if="food === '🍕'">Pizza!</div>
// </div>
// </div>
// `,
@ -294,29 +303,58 @@ import { html } from "./util";
// return { food: ref("nothing") };
// },
// };
//
// const app = new App();
// app.register("child", child);
// app.mount(main, "#app");
// ------------------------------------------------
// Practical :scope demo
const main = {
template: html`
<div class="hero sans-serif f2" :scope="{ visible: true }">
<div :show="visible">ON</div>
<div :show="!visible">OFF</div>
<button @click="() => visible = !visible" class="padding-x-1 padding-y-0_5 border-radius-1">Toggle</button>
</div>
`,
main() {
const onClick = () => {
console.log("ok");
};
// const main = {
// template: html`
// <div class="hero sans-serif f2" :scope="{ visible: true }">
// <div :show="visible">ON</div>
// <div :show="!visible">OFF</div>
// <button @click="() => visible = !visible" class="padding-x-1 padding-y-0_5 border-radius-1">Toggle</button>
// </div>
// `,
// main() {
// const onClick = () => {
// console.log("ok");
// };
//
// return { onClick };
// },
// };
//
// const app = new App();
// app.mount(main, "#app");
return { onClick };
},
};
// --------
// weird issue
// const child = {
// template: html`<div>child{{thing}}</div>`,
// props: { thing: { default: 1 }},
// main({ thing }) {
// return { thing };
// }
// };
const app = new App();
app.mount(main, "#app");
// const counter = {
// template: html`
// <div class="sans-serif">
// <div :teleport="body">
// <div :for="color in colors">{{color}}</div>
// </div>
// <child :teleport="body" .thing="5" />
// </div>
// `,
// main() {
// const colors = reactive(["red", "green"]);
// return { colors };
// },
// };
// const app = new App();
// app.register("child", child);
// app.mount(counter, "#app");

View File

@ -88,7 +88,6 @@ 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

@ -1,9 +1,12 @@
import { Block, Context } from "..";
import { nextTick } from "../util";
import { Block, Component, Context } from "..";
import { insertBefore } from "../util";
export function _teleport(el: Element, exp: string, ctx: Context) {
const anchor = new Comment(":teleport");
el.replaceWith(anchor);
export function _teleport(el: Element, exp: string, ctx: Context, component?: Component, componentProps?: Record<string, any>, allProps?: Record<string, any>) {
const anchor = new Comment(":teleport anchor");
insertBefore(anchor, el);
const observed = new Comment(":teleport");
el.replaceWith(observed);
console.log("Creating new block with allProps", component);
const target = document.querySelector(exp);
if (!target) {
@ -11,27 +14,40 @@ export function _teleport(el: Element, exp: string, ctx: Context) {
return;
}
nextTick(() => {
target.appendChild(el);
// Prevent interpolations flashing before they can be compiled due to nextTick,
// which is apparently required.
// @ts-ignore
const originalDisplay = el.style.display;
// @ts-ignore
el.style.display = "none";
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => {
if (removedNode.contains(anchor)) {
el.remove();
observer.disconnect();
}
});
let block: Block;
target.appendChild(el);
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => {
if (removedNode.contains(observed)) {
if (block.element) block.remove();
observer.disconnect();
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
observer.observe(document.body, { childList: true, subtree: true });
// Walks the tree of this teleported element.
new Block({
element: el,
parentContext: ctx,
});
// @ts-ignore
el.style.display = originalDisplay;
block = new Block({
element: el,
parentContext: ctx,
replacementType: "replace",
component,
componentProps,
allProps,
});
// Return the anchor so walk continues down the tree in the right order.

View File

@ -11,7 +11,7 @@ import { isComputed } from "./reactivity/computed";
import { effect as _effect } from "./reactivity/effect";
import { reactive } from "./reactivity/reactive";
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";
import { checkAndRemoveAttribute, componentHasPropByName, extractPropName, findSlotNodes, findTemplateNodes, insertBefore, isElement, isEventAttribute, isMirrorProp, isObject, isPropAttribute, isRegularProp, isSpreadProp, isText, nextTick, Slot, stringToElement, Template, toDisplayString } from "./util";
export * from "./plugins";
export * from "./plugins/router";
@ -64,7 +64,7 @@ export class App {
const root = typeof target === "string" ? (document.querySelector(target) as HTMLElement) : target;
const display = root.style.display;
root.style.display = "none";
this.root = this._mount(component, root, props);
this._mount(component, root, props);
root.style.display = display;
return this.root;
}
@ -81,6 +81,7 @@ export class App {
parentContext.scope.$isComputed = isComputed;
const block = new Block({
app: this,
element: target,
parentContext,
component,
@ -116,8 +117,7 @@ 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: reactive({}),
scope: parentContext ? parentContext.scope : reactive({}),
blocks: [],
effects: [],
slots: [],
@ -202,6 +202,7 @@ interface BlockOptions {
componentProps?: Record<string, any>;
allProps?: Record<string, any>;
parentContext?: Context;
app?: App;
component?: Component;
parentComponentBlock?: Block;
templates?: Template[];
@ -242,10 +243,11 @@ export class Block {
if (opts.isRoot) {
this.context = opts.parentContext;
opts.app.root = this;
} else {
this.parentContext = opts.parentContext ? opts.parentContext : createContext({});
this.parentContext.blocks.push(this);
this.context = createContext({ parentContext: opts.parentContext });
this.context = createContext({ parentContext: opts.parentContext, app: opts.app });
}
if (opts.component) {
@ -374,7 +376,6 @@ export class Block {
node = next;
}
} else {
// this.element.parentNode!.removeChild(this.element);
this.element.remove();
}
@ -415,8 +416,8 @@ function walk(node: Node, context: Context) {
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;
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
@ -429,15 +430,15 @@ function walk(node: Node, context: Context) {
}
}
if ((exp = checkAndRemoveAttribute(node, ":teleport"))) {
return _teleport(node, exp, 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 });
}
@ -448,8 +449,10 @@ function walk(node: Node, context: Context) {
new ValueDirective({ element: node, context, expression: exp });
}
if ((exp = checkAndRemoveAttribute(node, ":html"))) {
const htmlExp = exp;
context.effect(() => {
const result = evalGet(context.scope, exp, node);
const result = evalGet(context.scope, htmlExp, node);
if (result instanceof Element) {
node.replaceChildren();
node.append(result);
@ -459,8 +462,10 @@ function walk(node: Node, context: Context) {
});
}
if ((exp = checkAndRemoveAttribute(node, ":text"))) {
const textExp = exp;
context.effect(() => {
node.textContent = toDisplayString(evalGet(context.scope, exp, node));
node.textContent = toDisplayString(evalGet(context.scope, textExp, node));
});
}
};
@ -502,7 +507,8 @@ function walk(node: Node, context: Context) {
return new Block({
element: node,
parentContext: context,
app: current.componentBlock.context.app,
// parentContext: context,
component,
replacementType: "replace",
parentComponentBlock: current.componentBlock,

View File

@ -150,6 +150,10 @@ export function insertAfter(newNode: Node, existingNode: Node) {
}
}
export function insertBefore(newNode: Node, existingNode: Node) {
existingNode.parentNode?.insertBefore(newNode, existingNode);
}
export function isPropAttribute(attrName: string) {
if (attrName.startsWith(".")) {
return true;