diff --git a/README.md b/README.md index 6f538c4..774d8c0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # Async Framework -A lightweight, signal-based framework for building reactive web applications with custom elements and async handlers. +A lightweight, signal-based framework for building reactive web applications +with custom elements and async handlers. [![Async Framework](http://img.youtube.com/vi/mShb7a9znUg/0.jpg)](http://www.youtube.com/watch?v=mShb7a9znUg "Async Framework") ## Core Concepts 1. **Signals**: Reactive state management -2. **Custom Elements**: Web components with async capabilities +2. **Custom Elements**: Web components with async capabilities 3. **Event Handlers**: Async event handling with dynamic imports 4. **JSX Support**: Optional JSX/TSX support for component creation @@ -30,48 +31,51 @@ A lightweight, signal-based framework for building reactive web applications wit ### 1. Signals -Signals are reactive state containers that automatically track dependencies and update subscribers: +Signals are reactive state containers that automatically track dependencies and +update subscribers: + ```tsx - import { signal, computed } from '@async/framework'; +import { computed, signal } from "@async/framework"; - // Create a basic signal - const count = signal(0); +// Create a basic signal +const count = signal(0); - // Read and write to signal - console.log(count.value); // 0 - count.value = 1; +// Read and write to signal +console.log(count.value); // 0 +count.value = 1; - // Create a computed signal - const doubled = computed(() => count.value * 2); +// Create a computed signal +const doubled = computed(() => count.value * 2); ``` ### 2. Custom Elements Create reactive web components using signals: + ```tsx - // counter-element.js - import { signal } from '@async/framework'; +// counter-element.js +import { signal } from "@async/framework"; - export class CounterElement extends HTMLElement { - constructor() { - super(); - this.count = signal(0); - } +export class CounterElement extends HTMLElement { + constructor() { + super(); + this.count = signal(0); + } - connectedCallback() { - this.innerHTML = /*html*/` + connectedCallback() { + this.innerHTML = /*html*/ ` `; - - // Auto-update view when signal changes - const buttonEl = this.querySelector('button'); - this.count.subscribe(newValue => { - buttonEl.textContent = `Count: ${newValue}`; - }); - } - } + + // Auto-update view when signal changes + const buttonEl = this.querySelector("button"); + this.count.subscribe((newValue) => { + buttonEl.textContent = `Count: ${newValue}`; + }); + } +} // in main - customElements.define('counter-element', CounterElement); +customElements.define("counter-element", CounterElement); ``` ### 3. Async Event Handlers @@ -79,75 +83,73 @@ Create reactive web components using signals: Event handlers can be loaded asynchronously and chained: HTML: + ```html - - - - -
- Drag here -
+ + + + +
Drag here
``` Handler files: + ```tsx - // handlers/validate.js - export function handler(context) { - const { event, element } = context; - if (!element.value) { - context.break(); // Prevents next handlers from executing - return false; - } - } +// handlers/validate.js +export function handler(context) { + const { event, element } = context; + if (!element.value) { + context.break(); // Prevents next handlers from executing + return false; + } +} - // handlers/submit.js - export async function handler(context) { - const { event, element } = context; - const result = await submitData(element.value); - return result; - } +// handlers/submit.js +export async function handler(context) { + const { event, element } = context; + const result = await submitData(element.value); + return result; +} ``` ### 4. JSX Components Create components using JSX/TSX: + ```tsx - // Counter.tsx - import { signal } from '@async/framework'; - - export function Counter() { - const count = signal(0); - - return ( -
-

Count: {count}

- -
- ); - } +// Counter.tsx +import { signal } from "@async/framework"; + +export function Counter() { + const count = signal(0); + + return ( +
+

Count: {count}

+ +
+ ); +} ``` + ## Complete Example Here's a complete example combining all features: index.html: + ```html - - - - Codestin Search App - - -
- -
- - - - + + ``` + TodoApp.js: + ```tsx -import { ContextWrapper, html, signal, each, wrapContext } from "@async/framework"; +import { + ContextWrapper, + html, + signal, + each, + wrapContext, +} from "@async/framework"; export class TodoApp extends HTMLElement { private wrapper: ContextWrapper; @@ -188,13 +198,13 @@ export class TodoApp extends HTMLElement { const template = html`
- -
`; @@ -230,31 +243,32 @@ export class TodoApp extends HTMLElement { ``` Handlers: + ```tsx - // handlers/input.js - export function handler(context) { - const { element } = context; - const component = element.closest("todo-app"); - component.inputValue.value = element.value; - } +// handlers/input.js +export function handler(context) { + const { element } = context; + const component = element.closest("todo-app"); + component.inputValue.value = element.value; +} - // handlers/add-todo.js - export function handler(context) { +// handlers/add-todo.js +export function handler(context) { const { element } = context; - const component = element.closest("todo-app"); - const newTodo = component.inputValue.value.trim(); - if (newTodo) { - component.todos.value = [...component.todos.value, newTodo]; - } - } + const component = element.closest("todo-app"); + const newTodo = component.inputValue.value.trim(); + if (newTodo) { + component.todos.value = [...component.todos.value, newTodo]; + } +} - // handlers/clear-input.js - export function handler(context) { - const { element } = context; - const component = element.closest("todo-app"); - component.inputValue.value = ''; - context.element.querySelector('input').value = ''; - } +// handlers/clear-input.js +export function handler(context) { + const { element } = context; + const component = element.closest("todo-app"); + component.inputValue.value = ""; + context.element.querySelector("input").value = ""; +} ``` ## Key Features @@ -289,56 +303,60 @@ packages/ 1. Clone the repository 2. Install Deno if not already installed -3. Run example apps: - deno task start +3. Run example apps: deno task start Visit http://localhost:8000 to see the examples in action. # Framework Prompt -Use this prompt to help AI assistants understand how to work with this framework: +Use this prompt to help AI assistants understand how to work with this +framework: I'm using a custom web framework with the following characteristics: 1. It's built for Deno and uses TypeScript/JavaScript -2. Components should preferably be created using JSX/TSX (though Custom Elements are supported) +2. Components should preferably be created using JSX/TSX (though Custom Elements + are supported) 3. State management uses Signals (reactive state containers) 4. Event handling uses async handlers loaded dynamically BASIC SETUP: + - Create an index.html with this structure: + ```html - - Codestin Search App - - - -
- - - - + + Codestin Search App + + +
+ + + ``` + JSX COMPONENTS (Preferred Method): + - Create components in .tsx files - Use signals for state management Example App.tsx: + ```tsx -import { signal } from '@async/framework'; +import { signal } from "@async/framework"; export function App() { const count = signal(0); - + return (

Count: {count}

@@ -349,9 +367,11 @@ export function App() { ``` EVENT HANDLING: + - Events are handled using file paths in on: attributes - Multiple handlers can be chained with commas - Handlers receive a context object with: + ```jsonc { event, // Original DOM event @@ -381,33 +401,38 @@ EVENT HANDLING: Handler Patterns: 1. Default Export: + ```tsx - // handlers/submit.js - // typeof module.default === 'function' - export default function(context) { - // Used when no specific method is referenced - } +// handlers/submit.js +// typeof module.default === 'function' +export default function (context) { + // Used when no specific method is referenced +} ``` + 1. Named Event Handler: + ```tsx - // handlers/form.js - // "submit" -> "on" + capitalize("submit") - export function onSubmit(context) { - // Automatically matched when event name is "submit" - } +// handlers/form.js +// "submit" -> "on" + capitalize("submit") +export function onSubmit(context) { + // Automatically matched when event name is "submit" +} ``` 1. Hash-Referenced Export: + ```jsx - // handlers/drag.js - export function myCustomNamedHandler(context) {} - export function onDragend(context) {} - - // Use hash to target specific export -
- // dragend will resolve to onDragend -
+// handlers/drag.js +export function myCustomNamedHandler(context) {} +export function onDragend(context) {} + +// Use hash to target specific export +
+// dragend will resolve to onDragend +
``` + + 1. Inline Function (JSX): + ```tsx - - - -
- Drop Zone -
- - -
- -
+ + + + +
Drop Zone
+ + +
+ +
``` + Handler Context: + ```jsonc { event, // Original DOM event @@ -466,28 +493,31 @@ Handler Context: target, } ``` + Control Flow: + - Invoke context.break() to stop handler chain (rarely needed) - Return values are passed to next handler via context.value SIGNALS: + - Used for reactive state management - Created using signal(initialValue) - Access value with .value - Can be computed using computed(() => ...) - Separating get and set using createSignal(initialValue) -- Access value with [get, set] = createSignal() -Example: +- Access value with [get, set] = createSignal() Example: + ```tsx - const count = signal(0); - count.value++; // Updates all subscribers - const doubled = computed(() => count.value * 2); +const count = signal(0); +count.value++; // Updates all subscribers +const doubled = computed(() => count.value * 2); // passing around get and set - const [getCount, setCount] = createSignal(0); - setCount(getCount() + 1); // Updates all subscribers - const doubled = computed(() => getCount * 2); - +const [getCount, setCount] = createSignal(0); +setCount(getCount() + 1); // Updates all subscribers +const doubled = computed(() => getCount * 2); ``` + + FILE STRUCTURE: + ``` project/ ├── index.html @@ -512,6 +544,8 @@ project/ └── submit.js ``` -When working with this framework, please follow these conventions and patterns. The framework emphasizes clean separation of concerns, reactive state management, and async event handling. +When working with this framework, please follow these conventions and patterns. +The framework emphasizes clean separation of concerns, reactive state +management, and async event handling. END PROMPT diff --git a/packages/custom-element-signals/package.json b/packages/custom-element-signals/package.json index e8ffbe6..6bab47b 100644 --- a/packages/custom-element-signals/package.json +++ b/packages/custom-element-signals/package.json @@ -21,5 +21,11 @@ "bugs": { "url": "https://github.com/PatrickJS/async-framework/issues" }, - "homepage": "https://github.com/PatrickJS/async-framework#readme" + "homepage": "https://github.com/PatrickJS/async-framework#readme", + "dependencies": { + "typescript": "^5.6.3" + }, + "devDependencies": { + "tsc": "^2.0.4" + } } diff --git a/packages/custom-element-signals/src/let-signal.ts b/packages/custom-element-signals/src/let-signal.ts index e46a346..472a01b 100644 --- a/packages/custom-element-signals/src/let-signal.ts +++ b/packages/custom-element-signals/src/let-signal.ts @@ -2,7 +2,7 @@ import { parseAttributeValue } from "./utils/parse-attribute-value"; import { Signal } from "./signal-store"; import { SignalStoreInstance, signalStore } from "./signal-store-instance"; -// its better not to use this but ask the developer to do it otherwise there is a flicker +// There is no need to do this if everything is SSRd properly./ // const STYLE_ID = 'let-signal-style'; // if (!document.getElementById(STYLE_ID)) { // const style = document.createElement('style'); @@ -44,9 +44,11 @@ export class LetSignal extends HTMLElement { // we want to use some form of context here. maybe this.closest('let-signal-registry')? this._signalRegistry = window.signalRegistry || signalStore; } - get value(): T | undefined { + + get value(): T | void { return this.signal?.get(); } + set value(value: T) { this.signal?.set(value); } @@ -69,39 +71,8 @@ export class LetSignal extends HTMLElement { if (!name) { throw new Error("let-signal must have a name attribute"); } - // console.log('connectedCallback', name) - - const save = this.getAttribute("save"); - let value = undefined; - // use handler registry - if (save) { - const key = "signal-" + name; - const method = "getItem" in window[save] ? "getItem" : "get"; - const savedValue = window[save][method](key); - if (savedValue) { - try { - value = JSON.parse(savedValue); - } catch (e) { - console.error(`Error parsing saved value for ${save}: ${savedValue}`); - } - } - } - - this.signal = this.createSignal(name, value); - if (save) { - this.signal.subscribe((value) => { - if (window[save]) { - try { - const method = "setItem" in window[save] ? "setItem" : "set"; - const key = "signal-" + name; - window[save][method](key, JSON.stringify(value)); - } catch (e) { - console.error(`Error saving value for ${save}: ${value}`); - } - } - }); - } + this.signal = this.createSignal(name); } // This should never happen, but keeping it for completeness @@ -110,17 +81,17 @@ export class LetSignal extends HTMLElement { return; } if (!this.ready) { - // console.log('attributeChangedCallback hasConnectedCallback', name, oldValue, newValue, 'not connected') return; } if (name === "name") { - // console.log('attributeChangedCallback', name, oldValue, newValue) this._signalRegistry.delete(oldValue); - this.signal = this.createSignal(newValue, this.value); + this.signal = this.createSignal(newValue, this.signal?.get()); this._signalRegistry.set(newValue, this.signal); } else if (name === "value") { - this.value = parseAttributeValue(newValue); + const newParsedValue = parseAttributeValue(newValue); + this.value = newParsedValue; + this.signal?.set(newParsedValue); } } @@ -130,7 +101,6 @@ export class LetSignal extends HTMLElement { this._signalRegistry.delete(name); } this.ready = false; - // (this as any)._signalRegistry = null; this.signal?.cleanUp(); } }