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( declare function h(
type: string, type: string,
props: { [index: string]: any } | null, props?: { [index: string]: any } | null,
...children: (HTMLElement | string)[] ...children: (HTMLElement | string)[]
): Node; ): 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 }; type Stateful<T> = T & { readonly symbol: unique symbol };
declare function stateful<T>(target: T): Stateful<T>; 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 function css(strings: TemplateStringsArray, ...values: any): string;
declare var styled: { new: typeof css }; 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: // This wraps the target in a proxy, doing 2 things:
// - whenever a property is accessed, update the reference stack // - 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 // Hack to skip use() when there's only one possible property
Object.defineProperty(window, "h", { Object.defineProperty(window, "h", {
get: () => { get: () => {
@ -143,9 +148,40 @@ Object.defineProperty(window, "h", {
function h(type, props, ...children) { function h(type, props, ...children) {
if (typeof type === "function") { if (typeof type === "function") {
let newthis = stateful({}); 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 // insert an element at the end
useProp("after", callback => { useProp("after", callback => {
addChild(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 // apply the non-reactive properties
for (const name in props) { for (const name in props) {
const prop = props[name]; const prop = props[name];
@ -355,7 +396,7 @@ function JSXAddAttributes(elm, name, prop) {
return; return;
} }
if (typeof prop === "function" && name === "@mount") { if (typeof prop === "function" && name === "mount") {
prop(elm); prop(elm);
return; return;
} }
@ -386,11 +427,6 @@ function JSXAddAttributes(elm, name, prop) {
observer.observe(elm); observer.observe(elm);
return; return;
} }
if (name.startsWith("bind:")) {
const propname = name.substring(5);
prop[propname] = elm;
return;
}
elm.setAttribute(name, prop); elm.setAttribute(name, prop);
} }

View file

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

View file

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