Skip to content

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)

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.

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

user.service.ts
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.ts
import { 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

src/global/user.bridge.js
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;
}
}

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

user.component.html
<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

src/components/user/user.component.js
<div>
<p>User: {{ UserBridge.currentUser.name }} ({{ UserBridge.currentUser.role }})</p>
<button @click="UserBridge.setUser('Alice', 'Admin')">Set Admin</button>
</div>

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.

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.

  • 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 UserBridge directly in templates or actions.
  • Extending AvenxBridge: Ensure bridge classes extend AvenxBridge and execute super() inside their constructor before defining state properties.

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());
}
}
src/components/counter/counter.component.js
<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>

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“`

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 | async or manual subscribe()/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.
  • 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 async pipe operators are necessary.
  • Simplified State Mutations: Mutate proxy properties directly (state.count++) instead of calling signal.update(), signal.set(), or subject.next().
  • Services that push streams: Angular services often expose BehaviorSubjects that components subscribe to. In Avenx, share data through a Bridge (AvenxBridge in src/global/*.bridge.js) whose properties are read reactively in any component that touches them.