lunx.docs
DocsFrameworksAngular

Angular Adapter

Angular 17+ with Ivy compiler, Signals, standalone components, lazy-loaded routes, and RxJS — with SWC accelerating TypeScript compilation for faster builds.

Angular 17+ · Ivy · Signals
Ivy compiler

Full Angular Ivy compilation: ɵɵdefineComponent, ɵɵtemplate — producing optimal tree-shaken bundles.

Signals

Angular 17 Signals API (signal, computed, effect) for fine-grained reactivity without Zone.js overhead.

Standalone components

No NgModule required. Standalone components, directives, and pipes with direct imports.

RxJS compatible

Full RxJS 7+ support. Observable-based data fetching, event streams, and state management.

SWC TypeScript

SWC replaces tsc for transpilation — 10× faster type stripping. tsc is still used for type checking.

Lazy routes

loadComponent() and loadChildren() produce separate async chunks for route-level code splitting.

Setup

bash
npm create lunx-dev@latest my-angular-app -- --framework angular --ts
cd my-angular-app && npm install && npm run dev
typescriptlunx.config.ts
import { defineConfig } from 'lunx'

export default defineConfig({
  framework: 'angular',

  // Angular-specific options
  angular: {
    // Enable Angular Signals support
    signals: true,

    // Use esbuild-based browser builder (default for Angular 17+)
    builder: 'browser-esbuild',
  },
})

Standalone component with Signals

typescriptsrc/app/components/counter.component.ts
import { Component, signal, computed, effect } from '@angular/core'
import { CommonModule } from '@angular/common'

@Component({
  selector:    'app-counter',
  standalone:  true,
  imports:     [CommonModule],
  template: `
    <div class="counter">
      <h2>Count: {{ count() }}</h2>
      <p>Doubled: {{ doubled() }}</p>
      <div class="controls">
        <button (click)="decrement()">−</button>
        <button (click)="increment()">+</button>
        <button (click)="reset()" [disabled]="count() === 0">Reset</button>
      </div>
    </div>
  `,
  styles: [`
    .counter  { display: flex; flex-direction: column; gap: 1rem; padding: 1.5rem; }
    .controls { display: flex; gap: 0.5rem; }
    button    { padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; }
    button:disabled { opacity: 0.4; cursor: not-allowed; }
  `],
})
export class CounterComponent {
  count   = signal(0)
  doubled = computed(() => this.count() * 2)

  constructor() {
    // Runs whenever count changes — like useEffect
    effect(() => {
      console.log(`Count changed to ${this.count()}`)
    })
  }

  increment() { this.count.update(n => n + 1) }
  decrement() { this.count.update(n => n - 1) }
  reset()     { this.count.set(0) }
}

Standalone app bootstrap

typescriptsrc/main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideRouter }        from '@angular/router'
import { provideHttpClient }    from '@angular/common/http'
import { AppComponent }         from './app/app.component'
import { routes }               from './app/app.routes'

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
}).catch(console.error)

Lazy-loaded routes

typescriptsrc/app/app.routes.ts
import { Routes } from '@angular/router'

export const routes: Routes = [
  {
    path:            '',
    loadComponent:   () => import('./pages/home.component').then(m => m.HomeComponent),
  },
  {
    path:            'dashboard',
    loadComponent:   () => import('./pages/dashboard.component').then(m => m.DashboardComponent),
    canActivate:     [() => inject(AuthService).isLoggedIn()],
  },
  {
    path:            'admin',
    loadChildren:    () => import('./admin/admin.routes').then(m => m.adminRoutes),
  },
  {
    path:            '**',
    loadComponent:   () => import('./pages/not-found.component').then(m => m.NotFoundComponent),
  },
]

HTTP data fetching with rxjs

typescriptsrc/app/services/users.service.ts
import { Injectable, inject } from '@angular/core'
import { HttpClient }         from '@angular/common/http'
import { Observable }         from 'rxjs'
import { catchError, map }    from 'rxjs/operators'

export interface User {
  id:    number
  name:  string
  email: string
}

@Injectable({ providedIn: 'root' })
export class UsersService {
  private http = inject(HttpClient)
  private base = import.meta.env.LUNX_PUBLIC_API_URL

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${this.base}/users`).pipe(
      map(users => users.sort((a, b) => a.name.localeCompare(b.name))),
      catchError(err => { console.error(err); throw err })
    )
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.base}/users/${id}`)
  }
}
Zone.js and Signals
Angular 17 supports running without Zone.js when using Signals exclusively for change detection. In lunx.config.ts, set angular.zoneless: true to enable the provideExperimentalZonelessChangeDetection() provider automatically.