A lightweight, signal-based framework for building reactive web applications with custom elements and async handlers.
- Signals: Reactive state management
- Custom Elements: Web components with async capabilities
- Event Handlers: Async event handling with dynamic imports
- JSX Support: Optional JSX/TSX support for component creation
Component | Status | Description |
---|---|---|
AsyncLoader | Stable-ish | Core async loading functionality for handlers and modules |
HandlerRegistry | Stable-ish | Event handler registration and management system |
Framework Core | Unstable | Core framework features and utilities |
JSX Runtime | Unstable | JSX/TSX support and rendering (under development) |
Signals | Experimental | Reactive state management (API may change) |
Signal-List | Experimental | A signal-list primitive to optimize rendering lists |
Signal-Table | Experimental | A signal-table primitive to optimize rendering tables |
Custom Elements | Experimental | Web Components integration and lifecycle management |
Templates | Experimental | HTML template handling and instantiation |
QwikLoader | Experimental | Replace QwikLoader with AsyncLoader |
Signals are reactive state containers that automatically track dependencies and update subscribers:
import { signal, computed } from '@async/framework';
// Create a basic signal
const count = signal(0);
// Read and write to signal
console.log(count.value); // 0
count.value = 1;
// Create a computed signal
const doubled = computed(() => count.value * 2);
Create reactive web components using signals:
// counter-element.js
import { signal } from '@async/framework';
export class CounterElement extends HTMLElement {
constructor() {
super();
this.count = signal(0);
}
connectedCallback() {
this.innerHTML = /*html*/`
<button on:click="./handlers/increment.js">Count: ${this.count.value}</button>
`;
// 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);
Event handlers can be loaded asynchronously and chained:
HTML:
<!-- Multiple handlers separated by commas -->
<button
on:click="./handlers/validate.js, ./handlers/submit.js">
Submit
</button>
<!-- Handler with specific export -->
<div on:dragover="./handlers/drag.js#onDragover">
Drag here
</div>
Handler files:
// 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;
}
Create components using JSX/TSX:
// Counter.tsx
import { signal } from '@async/framework';
export function Counter() {
const count = signal(0);
return (
<div>
<h1>Count: {count}</h1>
<button on:click={() => count.value++}>
Increment
</button>
</div>
);
}
Here's a complete example combining all features:
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Async Framework Demo</title>
</head>
<body>
<div data-container="root">
<todo-app></todo-app>
</div>
<script type="module">
import { render } from "@async/framework";
import { TotoApp } from "./TodoApp.js";
// Register the custom element
customElements.define("todo-app", TotoApp);
// Render the component into the container
render(
document.querySelector("todo-app")
{
root: document.querySelector('[data-container="root"]'),
// events in the app
events: ["click", "keyup"],
},
);
</script>
</body>
</html>
TodoApp.js:
import { ContextWrapper, html, signal, each, wrapContext } from "@async/framework";
export class TodoApp extends HTMLElement {
private wrapper: ContextWrapper;
private todos;
private inputValue;
constructor() {
super();
this.wrapper = wrapContext(this, () => {
this.todos = signal<string[]>([]);
this.inputValue = signal("");
});
}
createTemplate() {
const template = html`
<div class="p-6 bg-white rounded-lg shadow-md">
<div class="mb-4 flex gap-2">
<input
type="text"
class="flex-1 px-4 py-2 border rounded"
value="${this.inputValue}"
on:keyup="./handlers/input.js"
>
<button
class="px-4 py-2 bg-indigo-600 text-white rounded"
on:click="./handlers/add-todo.js, ./handlers/clear-input.js"
>
Add Todo
</button>
</div>
<ul class="space-y-2">
${each(this.todos, (todo) => html`
<li class="flex items-center justify-between p-2 border rounded">
<span>${todo}</span>
<button
class="px-2 py-1 bg-red-500 text-white rounded"
on:click="./handlers/remove-todo.js"
>
Remove
</button>
</li>
`)}
</ul>
</div>
`;
return template;
}
connectedCallback() {
this.wrapper.render(() => this.createTemplate());
}
disconnectedCallback() {
this.wrapper.cleanup();
}
}
Handlers:
// 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) {
const { element } = context;
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 = '';
}
- π Reactive signals for state management
- β‘ Async event handlers with dynamic imports
- 𧩠Web Components integration
- βοΈ Optional JSX support
- π Pluggable architecture
- π¦ No build step required
- πͺΆ Lightweight and performant
- Keep handlers small and focused
- Use signals for shared state
- Leverage async handlers for complex operations
- Break down components into smaller, reusable pieces
- Use computed signals for derived state
packages/
examples/ # Example applications
async-loader/ # Core async loading functionality
dev/ # Development server
custom-element-signals/ # Custom element integration
- Clone the repository
- Install Deno if not already installed
- Run example apps: deno task start
Visit http://localhost:8000 to see the examples in action.
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:
- It's built for Deno and uses TypeScript/JavaScript
- Components should preferably be created using JSX/TSX (though Custom Elements are supported)
- State management uses Signals (reactive state containers)
- Event handling uses async handlers loaded dynamically
BASIC SETUP:
- Create an index.html with this structure:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { render } from '@async/framework';
import { App } from './App.tsx';
// Bootstrap the application
render(<App />, document.getElementById('app'));
</script>
</body>
</html>
JSX COMPONENTS (Preferred Method):
- Create components in .tsx files
- Use signals for state management
Example App.tsx:
import { signal } from '@async/framework';
export function App() {
const count = signal(0);
return (
<div>
<h1>Count: {count}</h1>
<button on:click="./handlers/increment.js">Add</button>
</div>
);
}
EVENT HANDLING:
- Events are handled using file paths in on: attributes
- Multiple handlers can be chained with commas
- Handlers receive a context object with:
{
event, // Original DOM event
element, // Target element
dispatch(), // Dispatch custom events
value, // Passed between chained handlers
// helpers
eventName, // Name of the event
attrValue, // Original attribute value
handlers, // Handler registry
signals, // Signal registry
templates, // Tenplate registry
container, // Container element
// TODO: component, // Component ref
module, // Module file instance of the handler
canceled, // If we canceled the chained handlers
break(), // break out of chained handlers
// mimic Event
preventDefault(),
stopPropagation(),
target,
}
Handler Patterns:
- Default Export:
// handlers/submit.js
// typeof module.default === 'function'
export default function(context) {
// Used when no specific method is referenced
}
- Named Event Handler:
// handlers/form.js
// "submit" -> "on" + capitalize("submit")
export function onSubmit(context) {
// Automatically matched when event name is "submit"
}
- Hash-Referenced Export:
// handlers/drag.js
export function myCustomNamedHandler(context) {}
export function onDragend(context) {}
// Use hash to target specific export
<div on:drag="./handlers/drag.js#myCustomNamedHandler" />
// dragend will resolve to onDragend
<div on:dragend="./handlers/drag.js" />
- Inline Function (JSX):
<button onClick={(context) => {
console.log('Clicked!', context);
}}>
Examples:
<!-- Chain multiple handler files -->
<button on:click="./handlers/validate.js, ./handlers/submit.js">
Submit
</button>
<!-- Target specific export with hash -->
<div on:dragover="./handlers/drag.js#onDragover">
Drop Zone
</div>
<!-- Use event-named export -->
<form on:submit="./handlers/form.js">
<!-- handler will use onSubmit export -->
</form>
Handler Context:
{
event, // Original DOM event
element, // Target element
dispatch(), // Dispatch custom events
value, // Passed between chained handlers
// helpers
eventName, // Name of the event
attrValue, // Original attribute value
handlers, // Handler registry
signals, // Signal registry
templates, // Tenplate registry
container, // Container element
// TODO: component, // Component ref
module, // Module file instance of the handler
canceled, // If we canceled the chained handlers
break(), // break out of chained handlers
// mimic Event
preventDefault(),
stopPropagation(),
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:
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);
FILE STRUCTURE:
project/
βββ index.html
βββ App.tsx
βββ components/
β βββ Counter.tsx
βββ handlers/
βββ increment.js
βββ 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.
END PROMPT