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