⚠️ Alpha Software — Adowire is in active development (0.x.x). APIs may change between minor versions. Not recommended for production use yet.
A full-stack reactive component system for AdonisJS v7 and Edge.js v6 — inspired by Laravel Livewire. Build dynamic, interactive UIs using server-side TypeScript classes and Edge templates. No frontend framework required.
You write a TypeScript class with public properties and methods, pair it with an Edge template, and Adowire handles everything else — state serialization, AJAX roundtrips, DOM diffing, two-way binding, validation, streaming, and more.
// No React. No Vue. No manual fetch calls.
// Just TypeScript + Edge templates = reactive UI.
- Requirements
- Installation
- Quick Start
- Components
- Rendering Components
- Page Components & Routing
- Lifecycle Hooks
- Decorators
- Client-Side Directives
- Validation
- Streaming
- Alpine.js Integration
- CLI Commands
- Configuration
- Security
- What Works & What's Coming
- License
| Dependency | Version |
|---|---|
| Node.js | >= 24.0.0 |
| AdonisJS | ^7.0.0 |
| Edge.js | ^6.0.0 |
| @adonisjs/assembler (optional, for CLI commands) | ^8.0.0 |
Install the package from npm:
npm install adowire
Then configure it using the Ace CLI:
node ace configure adowire
This will:
- Publish
config/adowire.tsto your project - Register the provider (
adowire/wire_provider) and commands (adowire/commands) inadonisrc.ts - Create the scaffold directories:
app/adowire/andresources/views/adowire/
In your main layout template (e.g. resources/views/layouts/main.edge), add the Adowire style and script tags:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? 'My App' }}</title>
@adowireStyles
</head>
<body>
{{{ await $slots.main() }}}
@adowireScripts
</body>
</html>
@adowireStyles— injects the[adowire:cloak]CSS rule to prevent FOUC@adowireScripts— injects the client-side JavaScript bundle and CSRF meta tag
node ace make:adowire counter
This generates two files:
app/adowire/counter.ts— the component classresources/views/adowire/counter.edge— the template
import { WireComponent } from 'adowire'
export default class Counter extends WireComponent {
count = 0
increment() {
this.count++
}
decrement() {
this.count--
}
reset() {
this.count = 0
}
}
<div>
<h2>Count: {{ count }}</h2>
<button adowire:click="increment">+</button>
<button adowire:click="decrement">−</button>
<button adowire:click="reset">Reset</button>
</div>
@layout('layouts/main')
@section('content')
@adowire('counter')
@end
@endsection
Or use the HTML-style syntax:
<adowire:counter />
A component is a TypeScript class that extends WireComponent. Public properties become reactive state. Public methods become callable actions.
import { WireComponent } from 'adowire'
export default class TodoList extends WireComponent {
// Reactive state — automatically synced with the client
items: string[] = []
newItem = ''
// Actions — callable from the template
add() {
if (this.newItem.trim()) {
this.items.push(this.newItem.trim())
this.newItem = ''
}
}
remove(index: number) {
this.items.splice(index, 1)
}
}
Every component has built-in methods for managing state:
| Method | Description |
|---|---|
fill(data) |
Bulk-assign properties from an object |
reset(...props) |
Reset properties to their initial values (all if no args) |
pull(...props) |
Reset and return the old values |
only(...props) |
Return a subset of public state |
all() |
Return all public state as a plain object |
These methods trigger special effects on the client:
| Method | Description |
|---|---|
$refresh() |
Re-render the component without calling any action |
$set(prop, value) |
Set a public property from the server |
$toggle(prop) |
Toggle a boolean property |
$redirect(url) |
Redirect the browser after the response |
$dispatch(event, params) |
Dispatch a browser event |
$stream(name, content, replace?) |
Stream content to a adowire:stream target |
$download(name, url) |
Trigger a file download |
js(expression) |
Execute a JavaScript expression on the client |
skipRender() |
Skip re-rendering for the current request |
{{-- Basic --}}
@adowire('counter')
@end
{{-- With props --}}
@adowire('counter', { initialCount: 10 })
@end
The HTML preprocessor transforms <adowire:...> tags at compile time:
{{-- Self-closing --}}
<adowire:counter />
{{-- With props --}}
<adowire:counter title="My Counter" />
{{-- Dynamic props (prefixed with :) --}}
<adowire:counter :count="someVariable" />
{{-- Boolean props --}}
<adowire:counter disabled />
{{-- Nested components (dot notation) --}}
<adowire:posts.create />
{{-- Kebab-case auto-converts to camelCase --}}
<adowire:counter initial-count="5" />
{{-- Dynamic component name --}}
<adowire:dynamic-component :is="activeTab" />
Page components are full-page Adowire components that replace traditional controller + view pairs. Use the router.adowire() macro in your routes file:
import router from '@adonisjs/core/services/router'
// Simple page
router.adowire('/dashboard', 'dashboard')
// With route params — passed to mount()
router.adowire('/posts/:id', 'posts.show')
Scaffold a page component with the --page flag:
node ace make:adowire dashboard --page
This generates a component with @Layout and @Title decorators:
import { WireComponent, Layout, Title } from 'adowire'
@Layout('layouts/adowire')
@Title('Dashboard')
export default class Dashboard extends WireComponent {
// Route params are passed to mount()
mount(params: Record<string, any>) {
// ...
}
}
| Hook | When It Runs | Request Type |
|---|---|---|
mount(props) |
First request only (component initialization) | Initial |
boot() |
Every request, before hydration | All |
hydrate() |
After boot, on subsequent requests only | AJAX |
rendering(view, data) |
Before Edge renders — can mutate data | All |
rendered(view, html) |
After Edge renders — can mutate HTML string | All |
dehydrate() |
End of every request, before snapshot | All |
updating(name, value) |
Before a property is set from the client | AJAX |
updated(name, value) |
After a property is set from the client | AJAX |
exception(error, stop) |
On unhandled exception | All |
You can define hooks that target a specific property by name:
// Called when 'search' is about to change
updatingSearch(value: string) {
console.log('Search changing to:', value)
}
// Called after 'search' has changed
updatedSearch(value: string) {
console.log('Search changed to:', value)
}
Memoized per-request computed property. Accessible in templates like a regular property.
import { WireComponent, Computed } from 'adowire'
export default class Cart extends WireComponent {
items: { price: number }[] = []
@Computed()
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
}
<p>Total: ${{ total }}</p>
Prevents a property from being modified by the client. Throws LockedPropertyException if tampered with.
import { WireComponent, Locked } from 'adowire'
export default class Payment extends WireComponent {
@Locked()
price = 99.99 // Client cannot change this
}
Attaches a VineJS validation rule to a property. Validated automatically on update or manually via this.validate().
import { WireComponent, Validate } from 'adowire'
import vine from '@vinejs/vine'
export default class Registration extends WireComponent {
@Validate(vine.string().email(), { message: 'Please enter a valid email' })
email = ''
@Validate(vine.string().minLength(8))
password = ''
}
Sets the browser's <title> for page components.
@Title('Settings')
export default class Settings extends WireComponent { /* ... */ }
Wraps a page component in a layout template.
@Layout('layouts/adowire', { slot: 'main' })
export default class Dashboard extends WireComponent { /* ... */ }
Directives are HTML attributes that wire up client-side behavior. All prefixed with adowire:.
Call a server-side method when the element is clicked.
<button adowire:click="increment">+1</button>
{{-- With arguments --}}
<button adowire:click="remove({{ id }})">Delete</button>
Two-way data binding between an input and a component property.
{{-- Deferred (synced on form submit) --}}
<input adowire:model="name" />
{{-- Live (syncs on every keystroke) --}}
<input adowire:model.live="search" />
{{-- Blur (syncs when the input loses focus) --}}
<input adowire:model.live.blur="email" />
{{-- Debounced (waits 500ms after the last keystroke) --}}
<input adowire:model.live.debounce.500ms="query" />
{{-- Throttled (at most once per second) --}}
<input adowire:model.live.throttle.1000ms="filter" />
Intercept form submission and call a server-side method. Automatically collects adowire:model fields.
<form adowire:submit="save">
<input adowire:model="title" />
<textarea adowire:model="body"></textarea>
<button type="submit">Save</button>
</form>
Show, hide, or modify elements during server requests.
{{-- Show while loading (hidden at rest) --}}
<div adowire:loading>Loading...</div>
{{-- Hide while loading (visible at rest) --}}
<div adowire:loading.remove>Content loaded.</div>
{{-- Add a CSS class while loading --}}
<button adowire:loading.class="opacity-50">Submit</button>
{{-- Remove a CSS class while loading --}}
<div adowire:loading.class.remove="opacity-100">Content</div>
{{-- Set an attribute while loading --}}
<button adowire:loading.attr="disabled">Submit</button>
Automatically refresh the component at an interval.
{{-- Poll every 2 seconds (default) --}}
<div adowire:poll>{{ timestamp }}</div>
{{-- Custom interval --}}
<div adowire:poll.5s>{{ notifications }}</div>
{{-- Only poll when the element is visible --}}
<div adowire:poll.10s.visible>{{ feed }}</div>
Show changes in the UI when local state differs from server state.
{{-- Show element when input is dirty --}}
<span adowire:dirty>Unsaved changes</span>
{{-- Add a class when dirty --}}
<input adowire:model.live="name" adowire:dirty.class="border-yellow-500" />
Toggle element visibility based on a JavaScript expression (no server roundtrip).
<div adowire:show="isOpen">Dropdown content</div>
Hide the element until the component has fully initialized. Prevents flash of unstyled content (FOUC).
<div adowire:cloak>This won't flash on load</div>
Target element for real-time SSE streaming via $stream().
{{-- Append streamed content --}}
<div adowire:stream="response"></div>
{{-- Replace content instead of appending --}}
<div adowire:stream.replace="status"></div>
Exclude an element from DOM morphing. Useful for third-party widgets.
<div adowire:ignore>This content won't be touched by morphdom</div>
Provide a stable identity for morphdom diffing (like key in React/Vue).
@each(item in items)
<div adowire:key="{{ item.id }}">{{ item.name }}</div>
@end
Adowire integrates with VineJS for server-side validation.
import { WireComponent, Validate } from 'adowire'
import vine from '@vinejs/vine'
export default class ContactForm extends WireComponent {
@Validate(vine.string().minLength(2))
name = ''
@Validate(vine.string().email())
email = ''
@Validate(vine.string().minLength(10))
message = ''
async submit() {
await this.validate() // Throws on failure, populates $errors
// If we get here, all fields are valid
}
}
import vine from '@vinejs/vine'
const validator = vine.compile(
vine.object({
name: vine.string(),
email: vine.string().email(),
})
)
export default class MyForm extends WireComponent {
name = ''
email = ''
async submit() {
const data = await this.validateUsing(validator)
// data is fully typed
}
}
<input adowire:model.live="email" />
@error('email')
<span class="text-red-500">{{ message }}</span>
@enderror
Send real-time content to the browser using Server-Sent Events:
export default class Chat extends WireComponent {
prompt = ''
async ask() {
for await (const chunk of getAIResponse(this.prompt)) {
this.$stream('answer', chunk)
}
}
}
<form adowire:submit="ask">
<input adowire:model="prompt" />
<button type="submit">Ask</button>
</form>
<div adowire:stream="answer"></div>
Adowire provides a $wire magic proxy for Alpine.js interop:
<div x-data>
{{-- Read state --}}
<span x-text="$wire.count"></span>
{{-- Set state --}}
<button @click="$wire.count = 0">Reset</button>
{{-- Call actions --}}
<button @click="await $wire.increment()">+1</button>
</div>
Adowire ships with 7 Ace commands for scaffolding and managing components.
Scaffold a new component class and/or Edge template.
# Component class + view
node ace make:adowire counter
# Nested component
node ace make:adowire posts/create
# Page component (adds @Layout + @Title decorators)
node ace make:adowire dashboard --page
# Class only (no view)
node ace make:adowire counter --class
# View only (no class)
node ace make:adowire counter --view
| Flag | Short | Description |
|---|---|---|
--page |
-p |
Scaffold as a page component with @Layout and @Title |
--class |
-c |
Generate only the TypeScript class |
--view |
-v |
Generate only the Edge template |
List all registered components.
node ace adowire:list
# Output as JSON
node ace adowire:list --json
| Flag | Short | Description |
|---|---|---|
--json |
-j |
Output the component list as JSON |
Create a default Adowire layout template.
node ace adowire:layout
# Custom name
node ace adowire:layout --name app
| Flag | Short | Description |
|---|---|---|
--name |
-n |
Layout file name (default: adowire) |
--force |
-f |
Overwrite if the file already exists |
Rename or move a component (both class and view), automatically updating the class name.
node ace adowire:move posts/create posts/entry
Delete a component's class and view files.
node ace adowire:delete counter
# Skip confirmation prompt
node ace adowire:delete counter --force
| Flag | Short | Description |
|---|---|---|
--force |
-f |
Skip the confirmation prompt |
Publish Adowire's stub templates to your project for customization.
node ace adowire:stubs
Stubs are published to stubs/vendor/adowire/.
| Flag | Short | Description |
|---|---|---|
--force |
-f |
Overwrite existing stubs |
Initial setup command (run once after installation).
node ace configure adowire
After running node ace configure adowire, you'll find config/adowire.ts in your project:
import { defineConfig } from 'adowire'
export default defineConfig({
// URL prefix for the Adowire message endpoint
prefix: '/adowire',
// Directory where component classes live
componentsPath: 'app/adowire',
// Edge.js view prefix for component templates
viewPrefix: 'adowire',
// Default layout for page components
defaultLayout: 'layouts/adowire',
// Enable dev-mode template variable proxy (warns on undefined access)
devProxy: true,
})
| Option | Default | Description |
|---|---|---|
prefix |
'/adowire' |
URL prefix for the message endpoint |
componentsPath |
'app/adowire' |
Where component classes live |
viewPrefix |
'adowire' |
Edge.js view prefix |
secret |
process.env.APP_KEY |
HMAC signing secret for snapshots |
namespaces |
— | Named namespace → directory mappings |
defaultLayout |
— | Fallback layout for router.adowire() pages |
devProxy |
true in dev |
Warns on undefined template variable access |
injectMorphMarkers |
true |
Morph markers around @if/@each blocks |
Adowire includes multiple layers of protection:
| Threat | Mitigation |
|---|---|
| Snapshot tampering | HMAC-SHA256 checksum on every snapshot, verified with timingSafeEqual |
| Locked property mutation | @Locked() throws LockedPropertyException before hydration |
| Calling private methods | $isCallable() blocks $/_ prefixed methods, lifecycle hooks, and reserved names |
| CSRF attacks | AdonisJS CSRF middleware; token sent via X-CSRF-TOKEN header |
| Sensitive data leakage | Protected/private properties are excluded from the client snapshot |
- Core reactive engine (snapshot, hydration, dehydration, DOM morphing)
- Public properties & two-way model binding
- Server-side actions callable from templates
- Full lifecycle hook system
- Real-time streaming (
$stream+ SSE) - 5 decorators (
@Computed,@Locked,@Validate,@Title,@Layout) - 10+ client-side directives (click, model, submit, loading, poll, dirty, show, cloak, stream, ignore, key)
- HTML-style component tags (
<adowire:counter />) - Page components with
router.adowire()macro - Alpine.js
$wirebridge - 7 Ace CLI commands
- VineJS validation integration
- Dev-mode template safety proxy
- HMAC snapshot security
- WireForm — dedicated form component with file uploads
- Events & Nesting —
$dispatch,@Ondecorator, parent/child communication - Islands & Lazy Loading — partial hydration,
@Lazydecorator - SPA Navigation —
adowire:navigatefor seamless page transitions - File Uploads — direct upload support with progress tracking
- Pagination — automatic cursor/offset pagination helpers
- URL State —
@Urldecorator for query string binding - Teleport — render component output into a different DOM location
- Testing Utilities —
WireTestharness for Japa
Contributions are welcome! Since the project is in alpha, please open an issue first to discuss what you'd like to change.
# Clone & install
git clone https://github.com/hackerrahul/adowire.git
cd adowire && npm install
# Build
npm run build
# Lint
npm run lint
MIT License © Rahul Gangotri