support 2 way data binding to components

This commit is contained in:
CoolElectronics 2024-01-01 12:43:02 -05:00
parent 0196435107
commit 01129edb48
No known key found for this signature in database
GPG key ID: F63593D168636C50
4 changed files with 75 additions and 31 deletions

9
AliceJS.d.ts vendored
View file

@ -4,20 +4,21 @@ declare namespace JSX {
declare function h(
type: string,
props: { [index: string]: any } | null,
props?: { [index: string]: any } | null,
...children: (HTMLElement | string)[]
): Node;
type AliceJSReferenceSink = { readonly __symbol: unique symbol };
type AliceJSReferenceSink<T> = { readonly __symbol: unique symbol, readonly __signature: T };
declare function use(sink: any, mapping?: (...args: any[]) => any): AliceJSReferenceSink;
declare function use<T>(sink: T | any, mapping?: (...args: any[]) => any): AliceJSReferenceSink<T>;
declare function useValue<T>(sink: AliceJSReferenceSink<T>): T;
type Stateful<T> = T & { readonly symbol: unique symbol };
declare function stateful<T>(target: T): Stateful<T>;
declare function handle(references: AliceJSReferenceSink, callback: (value: any) => void): void;
declare function handle<T>(references: AliceJSReferenceSink<T>, callback: (value: T) => void): void;
declare function css(strings: TemplateStringsArray, ...values: any): string;
declare var styled: { new: typeof css };

View file

@ -30,7 +30,7 @@ Object.defineProperty(window, "use", {
};
}
});
Object.assign(window, { stateful, handle, css, styled: { new: css } });
Object.assign(window, { stateful, handle, useValue, css, styled: { new: css } });
// This wraps the target in a proxy, doing 2 things:
// - whenever a property is accessed, update the reference stack
@ -122,6 +122,11 @@ export function handle(references, callback) {
}
}
export function useValue(references) {
let reference = references[references.length - 1];
return reference.proxy[reference.property];
}
// Hack to skip use() when there's only one possible property
Object.defineProperty(window, "h", {
get: () => {
@ -143,9 +148,40 @@ Object.defineProperty(window, "h", {
function h(type, props, ...children) {
if (typeof type === "function") {
let newthis = stateful({});
let component = type.bind(newthis);
return component();
for (const name in props) {
const references = props[name];
if (isAJSReferences(references) && name.startsWith("bind:")) {
let reference = references[references.length - 1];
const propname = name.substring(5);
if (propname == "this") {
reference.proxy[reference.property] = newthis;
} else {
// component two way data binding!! (exact same behavior as svelte:bind)
let isRecursive = false;
handle(references, value => {
if (isRecursive) {
isRecursive = false;
return;
}
isRecursive = true;
newthis[propname] = value
});
handle(use(newthis[propname]), value => {
if (isRecursive) {
isRecursive = false;
return;
}
isRecursive = true;
reference.proxy[reference.property] = value;
});
}
delete props[name];
}
}
return type.apply(newthis, [props]);
}
@ -310,23 +346,28 @@ function h(type, props, ...children) {
}
// two-way bind an <input>
useProp("$value", references => {
if (!isAJSReferences(references)) return;
if (!(elm instanceof HTMLInputElement)) return;
handle(references, value => elm.value = value);
let reference = references[references.length - 1];
elm.addEventListener("change", () => {
reference.proxy[reference.property] = elm.value;
})
});
// insert an element at the end
useProp("after", callback => {
addChild(callback());
})
for (const name in props) {
const references = props[name];
if (isAJSReferences(references) && name.startsWith("bind:")) {
let reference = references[references.length - 1];
const propname = name.substring(5);
if (propname == "this") {
reference.proxy[reference.property] = elm;
} else if (propname == "value") {
handle(references, value => elm.value = value);
elm.addEventListener("change", () => {
reference.proxy[reference.property] = elm.value;
})
}
delete props[name];
}
}
// apply the non-reactive properties
for (const name in props) {
const prop = props[name];
@ -355,7 +396,7 @@ function JSXAddAttributes(elm, name, prop) {
return;
}
if (typeof prop === "function" && name === "@mount") {
if (typeof prop === "function" && name === "mount") {
prop(elm);
return;
}
@ -386,11 +427,6 @@ function JSXAddAttributes(elm, name, prop) {
observer.observe(elm);
return;
}
if (name.startsWith("bind:")) {
const propname = name.substring(5);
prop[propname] = elm;
return;
}
elm.setAttribute(name, prop);
}

View file

@ -6,7 +6,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="index.css" />
<script src="../dist/index.js"></script>
<script src="../index.js"></script>
<script src="lib/index.js"></script>
</head>

View file

@ -1,4 +1,5 @@
function Counter() {
function Counter(a) {
console.log(a);
let css = styled.new`
self {
display: flex;
@ -21,7 +22,7 @@ function Counter() {
}
`;
this.counter = 0;
this.counter ??= 0;
return (
<div css={css} class="box">
@ -62,7 +63,7 @@ function ToDoList() {
return (
<div class="box" css={css}>
<div>
<input $value={use(this.text)} on:change={() => addTask()} />
<input bind:value={use(this.text)} on:change={() => addTask()} />
<button on:click={() => addTask()}>Add Task</button>
</div>
<div for={use(this.tasks)} do={(task, i) =>
@ -99,16 +100,22 @@ function Index() {
}
`;
this.c = 5;
this.counterobj;
return (
<div css={css}>
<div className={"as"}>
<div>
<h1>AliceJS Examples</h1>
<p>Some examples of AliceJS components. Code is in examples/</p>
</div>
<examples>
<Counter />
<Counter a="b" bind:this={use(this.counterobj)} bind:counter={use(this.c)} />
<ToDoList />
</examples>
stuff: {use(this.counterobj.counter)}
<button on:click={() => this.counterobj.counter++}>as</button>
</div>
);
}