mirror of
https://github.com/nvms/soma3.git
synced 2025-12-13 06:40:52 +00:00
ok
This commit is contained in:
parent
e0f4945b1b
commit
02730daae5
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
node_modules
|
||||
.aider*
|
||||
|
||||
300
public/demo.js
300
public/demo.js
@ -412,11 +412,12 @@ function _if(el, exp, ctx, component, componentProps, allProps) {
|
||||
let block;
|
||||
let activeBranchIndex = -1;
|
||||
const removeActiveBlock = () => {
|
||||
if (block) {
|
||||
parent.insertBefore(anchor, block.element);
|
||||
block.remove();
|
||||
block = void 0;
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
parent.insertBefore(anchor, block.element);
|
||||
block.remove();
|
||||
block = void 0;
|
||||
};
|
||||
ctx.effect(() => {
|
||||
for (let i = 0; i < branches.length; i++) {
|
||||
@ -730,18 +731,6 @@ var $computed = Symbol("computed");
|
||||
function isComputed(value) {
|
||||
return isObject(value) && value[$computed];
|
||||
}
|
||||
function computed(getter) {
|
||||
const ref2 = {
|
||||
get value() {
|
||||
return getter();
|
||||
},
|
||||
[$computed]: true
|
||||
};
|
||||
effect(() => {
|
||||
getter();
|
||||
});
|
||||
return ref2;
|
||||
}
|
||||
|
||||
// src/reactivity/ref.ts
|
||||
var $ref = Symbol("ref");
|
||||
@ -859,7 +848,7 @@ var App2 = class {
|
||||
}
|
||||
mount(component, target = "body", props = {}) {
|
||||
const root = typeof target === "string" ? document.querySelector(target) : target;
|
||||
const display = root.style.display;
|
||||
const { display } = root.style;
|
||||
root.style.display = "none";
|
||||
this._mount(component, root, props);
|
||||
root.style.display = display;
|
||||
@ -873,7 +862,7 @@ var App2 = class {
|
||||
}
|
||||
parentContext.scope.$isRef = isRef;
|
||||
parentContext.scope.$isComputed = isComputed;
|
||||
const block = new Block({
|
||||
return new Block({
|
||||
app: this,
|
||||
element: target,
|
||||
parentContext,
|
||||
@ -882,7 +871,6 @@ var App2 = class {
|
||||
componentProps: props,
|
||||
replacementType: "replaceChildren"
|
||||
});
|
||||
return block;
|
||||
}
|
||||
unmount() {
|
||||
this.root.teardown();
|
||||
@ -890,7 +878,7 @@ var App2 = class {
|
||||
};
|
||||
function createContext({ parentContext, app: app2 }) {
|
||||
const context = {
|
||||
app: app2 ? app2 : parentContext && parentContext.app ? parentContext.app : null,
|
||||
app: app2 ? app2 : parentContext?.app ? parentContext.app : null,
|
||||
scope: parentContext ? parentContext.scope : reactive({}),
|
||||
blocks: [],
|
||||
effects: [],
|
||||
@ -944,7 +932,7 @@ function mergeProps(props, defaultProps) {
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
var current = { componentBlock: void 0 };
|
||||
var current2 = { componentBlock: void 0 };
|
||||
var Block = class {
|
||||
element;
|
||||
context;
|
||||
@ -962,17 +950,15 @@ var Block = class {
|
||||
this.isFragment = opts.element instanceof HTMLTemplateElement;
|
||||
this.parentComponentBlock = opts.parentComponentBlock;
|
||||
if (opts.component) {
|
||||
current.componentBlock = this;
|
||||
current2.componentBlock = this;
|
||||
this.element = stringToElement(opts.component.template);
|
||||
} else if (this.isFragment) {
|
||||
this.element = opts.element.content.cloneNode(true);
|
||||
} else if (typeof opts.element === "string") {
|
||||
this.element = stringToElement(opts.element);
|
||||
} else {
|
||||
if (this.isFragment) {
|
||||
this.element = opts.element.content.cloneNode(true);
|
||||
} else if (typeof opts.element === "string") {
|
||||
this.element = stringToElement(opts.element);
|
||||
} else {
|
||||
this.element = opts.element.cloneNode(true);
|
||||
opts.element.replaceWith(this.element);
|
||||
}
|
||||
this.element = opts.element.cloneNode(true);
|
||||
opts.element.replaceWith(this.element);
|
||||
}
|
||||
if (opts.isRoot) {
|
||||
this.context = opts.parentContext;
|
||||
@ -1026,10 +1012,8 @@ var Block = class {
|
||||
if (opts.element instanceof HTMLElement) {
|
||||
opts.element.replaceWith(this.element);
|
||||
}
|
||||
} else {
|
||||
if (opts.element instanceof HTMLElement) {
|
||||
opts.element.replaceChildren(this.element);
|
||||
}
|
||||
} else if (opts.element instanceof HTMLElement) {
|
||||
opts.element.replaceChildren(this.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1111,112 +1095,111 @@ function walk(node, context) {
|
||||
new InterpolationDirective({ element: node, context });
|
||||
return;
|
||||
}
|
||||
if (isElement(node)) {
|
||||
let exp;
|
||||
const handleDirectives = (node2, context2, component, componentProps, allProps) => {
|
||||
if (warnInvalidDirectives(node2, [":if", ":for"])) return;
|
||||
if (warnInvalidDirectives(node2, [":for", ":teleport"])) return;
|
||||
if (warnInvalidDirectives(node2, [":if", ":teleport"])) return;
|
||||
if (exp = checkAndRemoveAttribute(node2, ":scope")) {
|
||||
const scope = evalGet(context2.scope, exp, node2);
|
||||
if (typeof scope === "object") {
|
||||
Object.assign(context2.scope, scope);
|
||||
}
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":if")) {
|
||||
return _if(node2, exp, context2, component, componentProps, allProps);
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":for")) {
|
||||
return _for(node2, exp, context2, component, componentProps, allProps);
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":teleport")) {
|
||||
return _teleport(node2, exp, context2, component, componentProps, allProps);
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":show")) {
|
||||
new ShowDirective({ element: node2, context: context2, expression: exp });
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":ref")) {
|
||||
context2.scope[exp].value = node2;
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":value")) {
|
||||
new ValueDirective({ element: node2, context: context2, expression: exp });
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":html")) {
|
||||
const htmlExp = exp;
|
||||
context2.effect(() => {
|
||||
const result = evalGet(context2.scope, htmlExp, node2);
|
||||
if (result instanceof Element) {
|
||||
node2.replaceChildren();
|
||||
node2.append(result);
|
||||
} else {
|
||||
node2.innerHTML = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":text")) {
|
||||
const textExp = exp;
|
||||
context2.effect(() => {
|
||||
node2.textContent = toDisplayString(evalGet(context2.scope, textExp, node2));
|
||||
});
|
||||
}
|
||||
};
|
||||
const processAttributes = (node2, component) => {
|
||||
return Array.from(node2.attributes).filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)).map((attr) => ({
|
||||
isMirror: isMirrorProp(attr.name),
|
||||
isSpread: isSpreadProp(attr.name),
|
||||
isBind: attr.name.includes("bind"),
|
||||
originalName: attr.name,
|
||||
extractedName: extractPropName(attr.name),
|
||||
exp: attr.value,
|
||||
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node2) : attr.value ? evalGet(context.scope, attr.value, node2) : void 0
|
||||
}));
|
||||
};
|
||||
if (isComponent(node, context)) {
|
||||
const component = context.app.getComponent(node.tagName.toLowerCase());
|
||||
const allProps = processAttributes(node, component);
|
||||
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
|
||||
if (isSpread) {
|
||||
const spread = evalGet(context.scope, extractedName, node);
|
||||
if (isObject(spread)) Object.assign(acc, spread);
|
||||
} else if (isMirror) {
|
||||
acc[extractedName] = evalGet(context.scope, extractedName, node);
|
||||
} else {
|
||||
acc[extractedName] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const next2 = handleDirectives(node, context, component, componentProps, allProps);
|
||||
if (next2) return next2;
|
||||
const templates = findTemplateNodes(node);
|
||||
return new Block({
|
||||
element: node,
|
||||
app: current.componentBlock.context.app,
|
||||
// parentContext: context,
|
||||
component,
|
||||
replacementType: "replace",
|
||||
parentComponentBlock: current.componentBlock,
|
||||
templates,
|
||||
componentProps,
|
||||
allProps
|
||||
}).element;
|
||||
}
|
||||
const next = handleDirectives(node, context);
|
||||
if (next) return next;
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
if (isPropAttribute(attr.name)) {
|
||||
new AttributeDirective({ element: node, context, attr });
|
||||
}
|
||||
if (isEventAttribute(attr.name)) {
|
||||
new EventDirective({ element: node, context, attr });
|
||||
}
|
||||
});
|
||||
walkChildren(node, context);
|
||||
if (!isElement(node)) {
|
||||
return;
|
||||
}
|
||||
let exp;
|
||||
const handleDirectives = (node2, context2, component, componentProps, allProps) => {
|
||||
if (warnInvalidDirectives(node2, [":if", ":for"])) return;
|
||||
if (warnInvalidDirectives(node2, [":for", ":teleport"])) return;
|
||||
if (warnInvalidDirectives(node2, [":if", ":teleport"])) return;
|
||||
if (exp = checkAndRemoveAttribute(node2, ":scope")) {
|
||||
const scope = evalGet(context2.scope, exp, node2);
|
||||
if (typeof scope === "object") {
|
||||
Object.assign(context2.scope, scope);
|
||||
}
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":if")) {
|
||||
return _if(node2, exp, context2, component, componentProps, allProps);
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":for")) {
|
||||
return _for(node2, exp, context2, component, componentProps, allProps);
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":teleport")) {
|
||||
return _teleport(node2, exp, context2, component, componentProps, allProps);
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":show")) {
|
||||
new ShowDirective({ element: node2, context: context2, expression: exp });
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":ref")) {
|
||||
context2.scope[exp].value = node2;
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":value")) {
|
||||
new ValueDirective({ element: node2, context: context2, expression: exp });
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":html")) {
|
||||
const htmlExp = exp;
|
||||
context2.effect(() => {
|
||||
const result = evalGet(context2.scope, htmlExp, node2);
|
||||
if (result instanceof Element) {
|
||||
node2.replaceChildren();
|
||||
node2.append(result);
|
||||
} else {
|
||||
node2.innerHTML = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (exp = checkAndRemoveAttribute(node2, ":text")) {
|
||||
const textExp = exp;
|
||||
context2.effect(() => {
|
||||
node2.textContent = toDisplayString(evalGet(context2.scope, textExp, node2));
|
||||
});
|
||||
}
|
||||
};
|
||||
const processAttributes = (node2, component) => Array.from(node2.attributes).filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)).map((attr) => ({
|
||||
isMirror: isMirrorProp(attr.name),
|
||||
isSpread: isSpreadProp(attr.name),
|
||||
isBind: attr.name.includes("bind"),
|
||||
originalName: attr.name,
|
||||
extractedName: extractPropName(attr.name),
|
||||
exp: attr.value,
|
||||
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node2) : attr.value ? evalGet(context.scope, attr.value, node2) : void 0
|
||||
}));
|
||||
if (isComponent(node, context)) {
|
||||
const component = context.app.getComponent(node.tagName.toLowerCase());
|
||||
const allProps = processAttributes(node, component);
|
||||
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
|
||||
if (isSpread) {
|
||||
const spread = evalGet(context.scope, extractedName, node);
|
||||
if (isObject(spread)) Object.assign(acc, spread);
|
||||
} else if (isMirror) {
|
||||
acc[extractedName] = evalGet(context.scope, extractedName, node);
|
||||
} else {
|
||||
acc[extractedName] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const next2 = handleDirectives(node, context, component, componentProps, allProps);
|
||||
if (next2) return next2;
|
||||
const templates = findTemplateNodes(node);
|
||||
return new Block({
|
||||
element: node,
|
||||
app: current2.componentBlock.context.app,
|
||||
// parentContext: context,
|
||||
component,
|
||||
replacementType: "replace",
|
||||
parentComponentBlock: current2.componentBlock,
|
||||
templates,
|
||||
componentProps,
|
||||
allProps
|
||||
}).element;
|
||||
}
|
||||
const next = handleDirectives(node, context);
|
||||
if (next) return next;
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
if (isPropAttribute(attr.name)) {
|
||||
new AttributeDirective({ element: node, context, attr });
|
||||
}
|
||||
if (isEventAttribute(attr.name)) {
|
||||
new EventDirective({ element: node, context, attr });
|
||||
}
|
||||
});
|
||||
walkChildren(node, context);
|
||||
}
|
||||
function walkChildren(node, context) {
|
||||
let child = node.firstChild;
|
||||
while (child) {
|
||||
child = walk(child, context) || child.nextSibling;
|
||||
let child2 = node.firstChild;
|
||||
while (child2) {
|
||||
child2 = walk(child2, context) || child2.nextSibling;
|
||||
}
|
||||
}
|
||||
var evalFuncCache = {};
|
||||
@ -1251,38 +1234,41 @@ function flattenRefs(scope) {
|
||||
const mapped = {};
|
||||
for (const key in scope) {
|
||||
if (scope.hasOwnProperty(key)) {
|
||||
if (isRef(scope[key])) {
|
||||
mapped[key] = scope[key].value;
|
||||
} else {
|
||||
mapped[key] = scope[key];
|
||||
}
|
||||
mapped[key] = isRef(scope[key]) ? scope[key].value : scope[key];
|
||||
}
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
// src/demo.ts
|
||||
var main = {
|
||||
var child = {
|
||||
template: html`
|
||||
<div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
|
||||
<h1 class="f1 margin-bottom-1 color-60">phase</h1>
|
||||
<h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
|
||||
<grid columns="6" class="f6 white-space-nowrap">
|
||||
<div :for="variant in ['base', 'accent', 'red', 'rose', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'lavender', 'violet', 'purple', 'plum', 'fuchsia', 'pink', 'peach']" class="border-color-30 border-2px">
|
||||
<div :for="rank, index in ranks">
|
||||
<div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</grid>
|
||||
<div>
|
||||
I am child and I have a cheeseburger: "{{food}}" (does not inherit)
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
main() {
|
||||
const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
|
||||
const basesReverse = computed(() => Array.from(ranks).reverse());
|
||||
const bg = (variant, rank, index) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
|
||||
return { ranks, bg };
|
||||
const food = ref("\u{1F354}");
|
||||
return { food };
|
||||
}
|
||||
};
|
||||
var main = {
|
||||
template: html`
|
||||
<div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
|
||||
<div :scope="{ food: '🍕' }">
|
||||
<div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div>
|
||||
<child>Child slot, food: {{food}} {{drink}}</child>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
main() {
|
||||
return { food: ref("nothing") };
|
||||
}
|
||||
};
|
||||
var app = new App2();
|
||||
app.register("child", child);
|
||||
app.mount(main, "#app");
|
||||
//# sourceMappingURL=demo.js.map
|
||||
|
||||
File diff suppressed because one or more lines are too long
111
src/demo.ts
111
src/demo.ts
@ -1,6 +1,7 @@
|
||||
import { App } from ".";
|
||||
import { computed } from "./reactivity/computed";
|
||||
import { reactive } from "./reactivity/reactive";
|
||||
import { ref } from "./reactivity/ref";
|
||||
import { html } from "./util";
|
||||
|
||||
// ------------------------------------------------
|
||||
@ -188,15 +189,15 @@ import { html } from "./util";
|
||||
// const style = reactive({ color: "gray" });
|
||||
// const increment = () => count.value++;
|
||||
// const decrement = () => count.value--;
|
||||
//
|
||||
|
||||
// setInterval(() => {
|
||||
// style.color = style.color === "gray" ? "white" : "gray";
|
||||
// }, 500);
|
||||
//
|
||||
|
||||
// return { count, increment, decrement, style };
|
||||
// },
|
||||
// };
|
||||
//
|
||||
|
||||
// const app = new App();
|
||||
// app.mount(counter, "#app");
|
||||
|
||||
@ -219,11 +220,11 @@ import { html } from "./util";
|
||||
// main() {
|
||||
// const items = reactive([1, 2, 3, 4, 5]);
|
||||
// const bool = ref(true);
|
||||
// setInterval(() => (bool.value = !bool.value), 250);
|
||||
// setInterval(() => (bool.value = !bool.value), 2050);
|
||||
// return { items, bool };
|
||||
// },
|
||||
// };
|
||||
//
|
||||
|
||||
// const app = new App();
|
||||
// app.mount(main, "#app");
|
||||
|
||||
@ -249,67 +250,67 @@ import { html } from "./util";
|
||||
|
||||
// ------------------------------------------------
|
||||
// Colors from css framework
|
||||
const main = {
|
||||
template: html`
|
||||
<div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
|
||||
<h1 class="f1 margin-bottom-1 color-60">phase</h1>
|
||||
<h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
|
||||
<grid columns="6" class="f6 white-space-nowrap">
|
||||
<div :for="variant in ['base', 'accent', 'red', 'rose', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'lavender', 'violet', 'purple', 'plum', 'fuchsia', 'pink', 'peach']" class="border-color-30 border-2px">
|
||||
<div :for="rank, index in ranks">
|
||||
<div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</grid>
|
||||
</div>
|
||||
`,
|
||||
main() {
|
||||
const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
|
||||
const basesReverse = computed(() => Array.from(ranks).reverse());
|
||||
const bg = (variant: string, rank: string, index: number) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
|
||||
|
||||
return { ranks, bg };
|
||||
},
|
||||
};
|
||||
|
||||
const app = new App();
|
||||
app.mount(main, "#app");
|
||||
|
||||
// ------------------------------------------------
|
||||
// :scope
|
||||
// const child = {
|
||||
// template: html`
|
||||
// <div>
|
||||
// I am child and I have food: "{{food}}" (does not inherit)
|
||||
// <div>
|
||||
// <slot />
|
||||
// </div>
|
||||
// </div>
|
||||
// `,
|
||||
// main() {
|
||||
// const food = ref("🍔");
|
||||
// return { food };
|
||||
// },
|
||||
// };
|
||||
|
||||
// const main = {
|
||||
// template: html`
|
||||
// <div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
|
||||
// <div :scope="{ food: '🍕' }">
|
||||
// <div>Scoped food: {{food}} and scoped drink: {{drink}}</div>
|
||||
// <child>Child slot, food: {{food}} {{drink}}</child>
|
||||
// </div>
|
||||
// <div class="sans-serif margin-y-3 container" style="--column-gap: .5rem; --row-gap: .5rem;">
|
||||
// <h1 class="f1 margin-bottom-1 color-60">phase</h1>
|
||||
// <h2 class="f3 margin-bottom-1 color-peach-50">Colors</h2>
|
||||
// <grid columns="6" class="f6 white-space-nowrap">
|
||||
// <div :for="variant in ['base', 'accent', 'red', 'rose', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'lavender', 'violet', 'purple', 'plum', 'fuchsia', 'pink', 'peach']" class="border-color-30 border-2px">
|
||||
// <div :for="rank, index in ranks">
|
||||
// <div .style:bind="bg(variant, rank, index)" class="padding-1">{{variant}}-{{rank}}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </grid>
|
||||
// </div>
|
||||
// `,
|
||||
// main() {
|
||||
// return { food: ref("nothing") };
|
||||
// const ranks = reactive(["5", "10", "20", "30", "40", "50", "60", "70", "80", "90"]);
|
||||
// const basesReverse = computed(() => Array.from(ranks).reverse());
|
||||
// const bg = (variant: string, rank: string, index: number) => ({ backgroundColor: `var(--${variant}-${rank})`, color: `var(--${variant}-${basesReverse.value[index]})` });
|
||||
|
||||
// return { ranks, bg };
|
||||
// },
|
||||
// };
|
||||
|
||||
// const app = new App();
|
||||
// app.register("child", child);
|
||||
// app.mount(main, "#app");
|
||||
|
||||
// ------------------------------------------------
|
||||
// :scope
|
||||
const child = {
|
||||
template: html`
|
||||
<div>
|
||||
I am child and I have a cheeseburger: "{{food}}" (does not inherit)
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
main() {
|
||||
const food = ref("🍔");
|
||||
return { food };
|
||||
},
|
||||
};
|
||||
|
||||
const main = {
|
||||
template: html`
|
||||
<div class="hero sans-serif f2" :scope="{ drink: '🍹' }">
|
||||
<div :scope="{ food: '🍕' }">
|
||||
<div>Parent has pizza: {{food}} and scoped drink: {{drink}}</div>
|
||||
<child>Child slot, food: {{food}} {{drink}}</child>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
main() {
|
||||
return { food: ref("nothing") };
|
||||
},
|
||||
};
|
||||
|
||||
const app = new App();
|
||||
app.register("child", child);
|
||||
app.mount(main, "#app");
|
||||
|
||||
// ------------------------------------------------
|
||||
// Practical :scope demo
|
||||
// const main = {
|
||||
|
||||
@ -35,11 +35,11 @@ export function _if(el: Element, exp: string, ctx: Context, component?: Componen
|
||||
let activeBranchIndex = -1;
|
||||
|
||||
const removeActiveBlock = () => {
|
||||
if (block) {
|
||||
parent.insertBefore(anchor, block.element);
|
||||
block.remove();
|
||||
block = undefined;
|
||||
}
|
||||
if (!block) { return; }
|
||||
|
||||
parent.insertBefore(anchor, block.element);
|
||||
block.remove();
|
||||
block = undefined;
|
||||
};
|
||||
|
||||
ctx.effect(() => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Context, evalGet } from "../";
|
||||
import { Context, current, evalGet } from "../";
|
||||
import { insertAfter, toDisplayString } from "../util";
|
||||
|
||||
interface InterpolationDirectiveOptions {
|
||||
|
||||
258
src/index.ts
258
src/index.ts
@ -80,7 +80,7 @@ export class App {
|
||||
|
||||
mount(component: Component, target: string | HTMLElement = "body", props: Record<string, any> = {}) {
|
||||
const root = typeof target === "string" ? (document.querySelector(target) as HTMLElement) : target;
|
||||
const display = root.style.display;
|
||||
const { display } = root.style;
|
||||
root.style.display = "none";
|
||||
this._mount(component, root, props);
|
||||
root.style.display = display;
|
||||
@ -98,7 +98,7 @@ export class App {
|
||||
parentContext.scope.$isRef = isRef;
|
||||
parentContext.scope.$isComputed = isComputed;
|
||||
|
||||
const block = new Block({
|
||||
return new Block({
|
||||
app: this,
|
||||
element: target,
|
||||
parentContext,
|
||||
@ -107,8 +107,6 @@ export class App {
|
||||
componentProps: props,
|
||||
replacementType: "replaceChildren",
|
||||
});
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
unmount() {
|
||||
@ -134,7 +132,7 @@ interface CreateContextOptions {
|
||||
|
||||
export function createContext({ parentContext, app }: CreateContextOptions): Context {
|
||||
const context: Context = {
|
||||
app: app ? app : parentContext && parentContext.app ? parentContext.app : null,
|
||||
app: app ? app : parentContext?.app ? parentContext.app : null,
|
||||
scope: parentContext ? parentContext.scope : reactive({}),
|
||||
blocks: [],
|
||||
effects: [],
|
||||
@ -251,15 +249,13 @@ export class Block {
|
||||
if (opts.component) {
|
||||
current.componentBlock = this;
|
||||
this.element = stringToElement(opts.component.template);
|
||||
} else if (this.isFragment) {
|
||||
this.element = (opts.element as HTMLTemplateElement).content.cloneNode(true) as Element;
|
||||
} else if (typeof opts.element === "string") {
|
||||
this.element = stringToElement(opts.element);
|
||||
} else {
|
||||
if (this.isFragment) {
|
||||
this.element = (opts.element as HTMLTemplateElement).content.cloneNode(true) as Element;
|
||||
} else if (typeof opts.element === "string") {
|
||||
this.element = stringToElement(opts.element);
|
||||
} else {
|
||||
this.element = opts.element.cloneNode(true) as Element;
|
||||
opts.element.replaceWith(this.element);
|
||||
}
|
||||
this.element = opts.element.cloneNode(true) as Element;
|
||||
opts.element.replaceWith(this.element);
|
||||
}
|
||||
|
||||
if (opts.isRoot) {
|
||||
@ -326,10 +322,8 @@ export class Block {
|
||||
if (opts.element instanceof HTMLElement) {
|
||||
opts.element.replaceWith(this.element);
|
||||
}
|
||||
} else {
|
||||
if (opts.element instanceof HTMLElement) {
|
||||
opts.element.replaceChildren(this.element);
|
||||
}
|
||||
} else if (opts.element instanceof HTMLElement) {
|
||||
opts.element.replaceChildren(this.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -432,128 +426,126 @@ function walk(node: Node, context: Context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isElement(node)) {
|
||||
let exp: string | null;
|
||||
if (!isElement(node)) { return; }
|
||||
|
||||
const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => {
|
||||
if (warnInvalidDirectives(node, [":if", ":for"])) return;
|
||||
if (warnInvalidDirectives(node, [":for", ":teleport"])) return;
|
||||
if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
|
||||
let exp: string | null;
|
||||
|
||||
// e.g. <div :scope="{ open: true }" />
|
||||
// In this case, the scope is merged into context.scope and will overwrite
|
||||
// anything returned from `main`.
|
||||
if ((exp = checkAndRemoveAttribute(node, ":scope"))) {
|
||||
const scope = evalGet(context.scope, exp, node);
|
||||
if (typeof scope === "object") {
|
||||
Object.assign(context.scope, scope);
|
||||
// context = createScopedContext(context, scope);
|
||||
}
|
||||
const handleDirectives = (node: Element, context: Context, component?: Component, componentProps?: Record<string, any>, allProps?: any[]) => {
|
||||
if (warnInvalidDirectives(node, [":if", ":for"])) return;
|
||||
if (warnInvalidDirectives(node, [":for", ":teleport"])) return;
|
||||
if (warnInvalidDirectives(node, [":if", ":teleport"])) return;
|
||||
|
||||
// e.g. <div :scope="{ open: true }" />
|
||||
// In this case, the scope is merged into context.scope and will overwrite
|
||||
// anything returned from `main`.
|
||||
if ((exp = checkAndRemoveAttribute(node, ":scope"))) {
|
||||
const scope = evalGet(context.scope, exp, node);
|
||||
if (typeof scope === "object") {
|
||||
Object.assign(context.scope, scope);
|
||||
// context = createScopedContext(context, scope);
|
||||
}
|
||||
|
||||
if ((exp = checkAndRemoveAttribute(node, ":if"))) {
|
||||
return _if(node, exp, context, component, componentProps, allProps);
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":for"))) {
|
||||
return _for(node, exp, context, component, componentProps, allProps);
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":teleport"))) {
|
||||
return _teleport(node, exp, context, component, componentProps, allProps);
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":show"))) {
|
||||
new ShowDirective({ element: node, context, expression: exp });
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":ref"))) {
|
||||
context.scope[exp].value = node;
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":value"))) {
|
||||
new ValueDirective({ element: node, context, expression: exp });
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":html"))) {
|
||||
const htmlExp = exp;
|
||||
|
||||
context.effect(() => {
|
||||
const result = evalGet(context.scope, htmlExp, node);
|
||||
if (result instanceof Element) {
|
||||
node.replaceChildren();
|
||||
node.append(result);
|
||||
} else {
|
||||
node.innerHTML = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":text"))) {
|
||||
const textExp = exp;
|
||||
|
||||
context.effect(() => {
|
||||
node.textContent = toDisplayString(evalGet(context.scope, textExp, node));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processAttributes = (node: Element, component?: Component) => {
|
||||
return Array.from(node.attributes)
|
||||
.filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || (isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)))
|
||||
.map((attr) => ({
|
||||
isMirror: isMirrorProp(attr.name),
|
||||
isSpread: isSpreadProp(attr.name),
|
||||
isBind: attr.name.includes("bind"),
|
||||
originalName: attr.name,
|
||||
extractedName: extractPropName(attr.name),
|
||||
exp: attr.value,
|
||||
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node) : attr.value ? evalGet(context.scope, attr.value, node) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
if (isComponent(node, context)) {
|
||||
const component = context.app.getComponent(node.tagName.toLowerCase());
|
||||
const allProps = processAttributes(node, component);
|
||||
|
||||
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
|
||||
if (isSpread) {
|
||||
const spread = evalGet(context.scope, extractedName, node);
|
||||
if (isObject(spread)) Object.assign(acc, spread);
|
||||
} else if (isMirror) {
|
||||
acc[extractedName] = evalGet(context.scope, extractedName, node);
|
||||
} else {
|
||||
acc[extractedName] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const next = handleDirectives(node, context, component, componentProps, allProps);
|
||||
if (next) return next;
|
||||
|
||||
const templates = findTemplateNodes(node);
|
||||
|
||||
return new Block({
|
||||
element: node,
|
||||
app: current.componentBlock.context.app,
|
||||
// parentContext: context,
|
||||
component,
|
||||
replacementType: "replace",
|
||||
parentComponentBlock: current.componentBlock,
|
||||
templates,
|
||||
componentProps,
|
||||
allProps,
|
||||
}).element;
|
||||
}
|
||||
|
||||
const next = handleDirectives(node, context);
|
||||
if ((exp = checkAndRemoveAttribute(node, ":if"))) {
|
||||
return _if(node, exp, context, component, componentProps, allProps);
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":for"))) {
|
||||
return _for(node, exp, context, component, componentProps, allProps);
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":teleport"))) {
|
||||
return _teleport(node, exp, context, component, componentProps, allProps);
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":show"))) {
|
||||
new ShowDirective({ element: node, context, expression: exp });
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":ref"))) {
|
||||
context.scope[exp].value = node;
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":value"))) {
|
||||
new ValueDirective({ element: node, context, expression: exp });
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":html"))) {
|
||||
const htmlExp = exp;
|
||||
|
||||
context.effect(() => {
|
||||
const result = evalGet(context.scope, htmlExp, node);
|
||||
if (result instanceof Element) {
|
||||
node.replaceChildren();
|
||||
node.append(result);
|
||||
} else {
|
||||
node.innerHTML = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
if ((exp = checkAndRemoveAttribute(node, ":text"))) {
|
||||
const textExp = exp;
|
||||
|
||||
context.effect(() => {
|
||||
node.textContent = toDisplayString(evalGet(context.scope, textExp, node));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processAttributes = (node: Element, component?: Component) => Array.from(node.attributes)
|
||||
.filter((attr) => isSpreadProp(attr.name) || isMirrorProp(attr.name) || (isRegularProp(attr.name) && componentHasPropByName(extractPropName(attr.name), component)))
|
||||
.map((attr) => ({
|
||||
isMirror: isMirrorProp(attr.name),
|
||||
isSpread: isSpreadProp(attr.name),
|
||||
isBind: attr.name.includes("bind"),
|
||||
originalName: attr.name,
|
||||
extractedName: extractPropName(attr.name),
|
||||
exp: attr.value,
|
||||
value: isMirrorProp(attr.name) ? evalGet(context.scope, extractPropName(attr.name), node) : attr.value ? evalGet(context.scope, attr.value, node) : undefined,
|
||||
}));
|
||||
|
||||
if (isComponent(node, context)) {
|
||||
const component = context.app.getComponent(node.tagName.toLowerCase());
|
||||
const allProps = processAttributes(node, component);
|
||||
|
||||
const componentProps = allProps.reduce((acc, { isSpread, isMirror, extractedName, value }) => {
|
||||
if (isSpread) {
|
||||
const spread = evalGet(context.scope, extractedName, node);
|
||||
if (isObject(spread)) Object.assign(acc, spread);
|
||||
} else if (isMirror) {
|
||||
acc[extractedName] = evalGet(context.scope, extractedName, node);
|
||||
} else {
|
||||
acc[extractedName] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const next = handleDirectives(node, context, component, componentProps, allProps);
|
||||
if (next) return next;
|
||||
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
if (isPropAttribute(attr.name)) {
|
||||
new AttributeDirective({ element: node, context, attr });
|
||||
}
|
||||
const templates = findTemplateNodes(node);
|
||||
|
||||
if (isEventAttribute(attr.name)) {
|
||||
new EventDirective({ element: node, context, attr });
|
||||
}
|
||||
});
|
||||
|
||||
walkChildren(node, context);
|
||||
return new Block({
|
||||
element: node,
|
||||
app: current.componentBlock.context.app,
|
||||
// parentContext: context,
|
||||
component,
|
||||
replacementType: "replace",
|
||||
parentComponentBlock: current.componentBlock,
|
||||
templates,
|
||||
componentProps,
|
||||
allProps,
|
||||
}).element;
|
||||
}
|
||||
|
||||
const next = handleDirectives(node, context);
|
||||
if (next) return next;
|
||||
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
if (isPropAttribute(attr.name)) {
|
||||
new AttributeDirective({ element: node, context, attr });
|
||||
}
|
||||
|
||||
if (isEventAttribute(attr.name)) {
|
||||
new EventDirective({ element: node, context, attr });
|
||||
}
|
||||
});
|
||||
|
||||
walkChildren(node, context);
|
||||
}
|
||||
|
||||
function walkChildren(node: Node, context: Context) {
|
||||
@ -605,11 +597,7 @@ function flattenRefs(scope: any): any {
|
||||
for (const key in scope) {
|
||||
if (scope.hasOwnProperty(key)) {
|
||||
// Check if the value is a Ref
|
||||
if (isRef(scope[key])) {
|
||||
mapped[key] = scope[key].value;
|
||||
} else {
|
||||
mapped[key] = scope[key];
|
||||
}
|
||||
mapped[key] = isRef(scope[key]) ? scope[key].value : scope[key];
|
||||
}
|
||||
}
|
||||
return mapped;
|
||||
|
||||
26
tests/app.test.ts
Normal file
26
tests/app.test.ts
Normal 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
38
tests/router.test.ts
Normal 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>");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user