Migrating from Angular to Avenx-JS
This guide details how to migrate applications built with Angular to Avenx-JS.
1. Architectural Overview & Mental Model Shift
Section titled “1. Architectural Overview & Mental Model Shift”Angular applications use TypeScript classes with @Component decorators, dependency injection trees, and RxJS/Signals. Avenx-JS provides a lightweight companion file architecture (.component.js and .component.css) with proxy-based state and global Bridges (AvenxBridge).
| Concept | Angular | Avenx-JS |
|---|---|---|
| Component Definition | TypeScript @Component class + HTML template |
.component.js (logic/template) + .component.css |
| Template Loops | *ngFor="let item of items" |
<@for item in state.items key="item.id"> |
| Shared State & Services | @Injectable({ providedIn: 'root' }) Services |
Bridges (AvenxBridge in src/global/*.bridge.js) |
| Reactivity | Signals (signal()) / RxJS Observables |
Proxy state (<state />) and <computed /> tags |
| Route Protection | Angular CanActivate guards |
AvenxGuard classes with canActivate(to, from) |
2. Component Anatomy and Template Syntax
Section titled “2. Component Anatomy and Template Syntax”This section will document replacing @Component classes and Angular directives (*ngFor, [ngClass], (click)) with Avenx companion files and template syntax.
3. Services and Dependency Injection Alternatives
Section titled “3. Services and Dependency Injection Alternatives”Angular centralizes shared business logic and global state in @Injectable({ providedIn: 'root' }) services injected into component constructors through its Dependency Injection (DI) framework. Avenx-JS replaces services and DI with Bridges — class-based reactive modules in src/global/*.bridge.js that extend AvenxBridge. See Shared State & Bridges for the full bridge API and Migration Overview for the high-level conceptual mapping.
Replacing @Injectable Services
Section titled “Replacing @Injectable Services”A service becomes a bridge class. Where Angular marks the class with @Injectable({ providedIn: 'root' }), Avenx declares it in src/global/<name>.bridge.js and extends AvenxBridge. State the service held as fields becomes reactive properties initialized in the bridge constructor; business logic stays as class methods.
Before — Angular @Injectable Service & Component DI
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root'})export class UserService { currentUser = { name: 'Guest', role: 'visitor' }; isLoggedIn = false;
setUser(name: string, role: string) { this.currentUser = { name, role }; this.isLoggedIn = true; }}
// user.component.tsimport { Component } from '@angular/core';import { UserService } from './user.service';
@Component({ selector: 'app-user', template: `<p>{{ userService.currentUser.name }}</p>` })export class UserComponent { constructor(public userService: UserService) {}}After — Avenx.js Bridge Class
import { AvenxBridge } from 'avenx-core/runtime';
export default class UserBridge extends AvenxBridge { constructor() { super(); this.currentUser = { name: 'Guest', role: 'visitor' }; this.isLoggedIn = false; }
setUser(name, role) { this.currentUser = { name, role }; this.isLoggedIn = true; }}Using a Bridge in a Component Template
Section titled “Using a Bridge in a Component Template”Bridges are automatically loaded and registered by the compiler. They are exposed directly to component templates and actions under their capitalized name postfixed with Bridge (e.g. UserBridge) — no import, provider, or constructor parameter needed.
Before — Angular template using the injected service
<p>{{ userService.currentUser.name }} ({{ userService.currentUser.role }})</p><button (click)="userService.setUser('Alice', 'Admin')">Set Admin</button>After — Avenx.js template using the bridge singleton
<div> <p>User: {{ UserBridge.currentUser.name }} ({{ UserBridge.currentUser.role }})</p> <button @click="UserBridge.setUser('Alice', 'Admin')">Set Admin</button></div>Eliminating Constructor Injection
Section titled “Eliminating Constructor Injection”Avenx components do not take constructor parameters. There is no DI container and no provider tree to configure; component constructors only initialize local component state. Shared logic is always reached by referencing the bridge directly in the template or inside <action> blocks.
Global Registration & Singleton Scope
Section titled “Global Registration & Singleton Scope”Every bridge in src/global/ is registered globally at compile time and instantiated once. All components and pages share the same bridge instance, so state written by one component is immediately visible to every other component that references the bridge — the Avenx equivalent of a root-provided singleton service.
Key Conceptual Differences & Pitfalls
Section titled “Key Conceptual Differences & Pitfalls”- No DI Hierarchy: Angular supports hierarchical injectors and scoping services to specific module trees. Avenx Bridges operate as global singletons; there is no per-module or per-route scoping.
- No Constructor Parameters: Component constructors in Avenx do not accept injected services. Simply reference
UserBridgedirectly in templates or actions. - Extending
AvenxBridge: Ensure bridge classes extendAvenxBridgeand executesuper()inside their constructor before defining state properties.
4. Signals and RxJS to Proxy Reactivity
Section titled “4. Signals and RxJS to Proxy Reactivity”Angular models reactive data with two APIs: Signals (signal(), computed(), effect()) for synchronous state, and RxJS Observables (BehaviorSubject, async pipe) for streams. Avenx-JS collapses both into transparent Proxy state: plain properties on a reactive state object that the framework watches and patches into the DOM for you. See the Reactive State guide for the full mental model, and the Migration Overview for how every framework maps onto it.
4.1 Replacing Signals & Observables with <state>
Section titled “4.1 Replacing Signals & Observables with <state>”A single <state> tag declares all of a component’s reactive properties. Values are plain JavaScript properties — there are no setter functions and no getter calls.
| Angular | Avenx-JS |
|---|---|
signal(0) |
<state count="0" /> → state.count |
signal.set(v) / signal.update(fn) |
state.count = v / state.count++ |
BehaviorSubject |
<state status="'Ready'" /> → state.status |
subject.next(v) |
state.status = v |
computed(() => expr) |
<computed name="doubleCount" value="state.count * 2" /> |
Before – Angular Signals & RxJS Async Pipe
Section titled “Before – Angular Signals & RxJS Async Pipe”import { Component, signal, computed } from '@angular/core';import { BehaviorSubject } from 'rxjs';
@Component({ selector: 'app-counter', template: ` <div> <p>Count: {{ count() }}</p> <p>Double: {{ doubleCount() }}</p> <p>Status: {{ status$ | async }}</p> <button (click)="increment()">Increment</button> </div> `})export class CounterComponent { count = signal(0); doubleCount = computed(() => this.count() * 2); status$ = new BehaviorSubject('Ready');
increment() { this.count.update(c => c + 1); this.status$.next('Updated at ' + new Date().toLocaleTimeString()); }}After – Avenx.js Proxy State & Computed
Section titled “After – Avenx.js Proxy State & Computed”<state count="0" status="'Ready'" /><computed name="doubleCount" value="state.count * 2" /><action name="increment"> state.count++; state.status = 'Updated at ' + new Date().toLocaleTimeString();</action>
<div> <p>Count: {{ state.count }}</p> <p>Double: {{ doubleCount }}</p> <p>Status: {{ state.status }}</p> <button @click="increment()">Increment</button></div>4.2 Replacing the async Pipe
Section titled “4.2 Replacing the async Pipe”Templates read reactive properties directly. There is no subscription unwrapping and no async pipe: interpolation ({{ state.count }}) already reflects the latest value, and every mutation schedules an automatic DOM patch.
| Angular template | Avenx-JS template |
|---|---|
{{ count() }} |
{{ state.count }} |
| `{{ status$ | async }}` |
| `*ngIf=“(user$ | async) as user“` |
| `*ngFor=“let item of items$ | async“` |
4.3 Replacing Streams with <resource>
Section titled “4.3 Replacing Streams with <resource>”RxJS is not needed for asynchronous data that changes over time. Avenx-JS provides the <resource> SFC tag & Resource API, which tracks reactive dependencies, re-fetches when they change, and integrates with <@suspense>:
// Angular: this.user$ = this.http.get<User>(`/api/users/${this.id}`);<!-- Avenx-JS --><resource name="user" handler="fetch(`/api/users/${state.userId}`).then(r => r.json())" />
<p>Name: {{ state.user?.name }}</p>4.4 Mental Model Shift: Push Streams → Declarative Proxy State
Section titled “4.4 Mental Model Shift: Push Streams → Declarative Proxy State”- No execution parentheses: Angular Signals are getter functions (
count()); Avenx state properties are read as plain values (state.count). - No
.next(),.set(), or.update(): mutating a proxy property (state.count++,state.status = ...) is the only API you need. - No subscription lifecycle: Angular requires
| asyncor manualsubscribe()/unsubscribe()management. Avenx components never subscribe; the framework’s watcher observes property reads and patches the DOM automatically, batching updates into a single microtask flush. - Derived values are cached, not recomputed: Angular
computed()lazily caches; Avenx<computed>does the same with automatic dependency tracking and circular-dependency protection.
4.5 Key Conceptual Differences & Pitfalls
Section titled “4.5 Key Conceptual Differences & Pitfalls”- No Execution Parentheses: Angular Signals require calling the signal getter function (
count()). Avenx state properties are plain proxy properties (state.count). - No Async Pipe Needed: Templates read reactive properties directly. No subscription unwrapping or
asyncpipe operators are necessary. - Simplified State Mutations: Mutate proxy properties directly (
state.count++) instead of callingsignal.update(),signal.set(), orsubject.next(). - Services that push streams: Angular services often expose
BehaviorSubjects that components subscribe to. In Avenx, share data through a Bridge (AvenxBridgeinsrc/global/*.bridge.js) whose properties are read reactively in any component that touches them.