Appearance
For Angular shops
The Angular template DSL is the canonical "I like my framework but I hate the syntax" pain point. *ngFor, decorator soup, [(ngModel)] ceremony, constructor-DI noise, the standalone-components migration tax — they're all things Angular users have asked their framework to fix for years.
Rozie is a Vue-flavored authoring layer that compiles to Angular 19+: standalone components, signals, the new @if / @for block syntax, input.required<T>(), model<T>(), output<T>(), inject(DestroyRef). You stay in Angular; only the authoring syntax changes.
You write one .rozie component this week. The compiled .ts drops into your existing Angular app as a standalone component. Nothing else changes.
What you write vs what Angular sees
Side by side — a debounced search input
This is the canonical examples/SearchInput.rozie file — the same one used as a working consumer in examples/consumers/angular-analogjs/, and the same one the SearchInput example page shows compiled to all six targets. The Angular output below is generated on every docs build by passing the Rozie source through the live compiler — it cannot drift.
What an Angular dev typically writes today
ts
// SearchInput.ts (hand-written Angular standalone component)
import {
Component, ElementRef, ViewEncapsulation,
computed, effect, inject, input, output, signal, viewChild,
DestroyRef, afterNextRender,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subject, debounceTime } from 'rxjs';
@Component({
selector: 'rz-search-input',
standalone: true,
imports: [FormsModule],
template: `
<div class="search-input">
<input
#inputEl
type="search"
[placeholder]="placeholder()"
[ngModel]="query()"
(ngModelChange)="onInput($event)"
(keydown.enter)="onSearch()"
(keydown.escape)="onClear()"
/>
@if (query().length > 0) {
<button class="clear-btn" (click)="onClear()" aria-label="Clear">×</button>
} @else {
<span class="hint">{{ minLength() }}+ chars</span>
}
</div>
`,
styles: [`
.search-input { display: inline-flex; align-items: center; gap: 0.25rem; }
input { padding: 0.25rem 0.5rem; }
.clear-btn { background: none; border: none; cursor: pointer; font-size: 1.25rem; }
.hint { color: rgba(0, 0, 0, 0.4); font-size: 0.85em; }
`],
})
export class SearchInput {
placeholder = input<string>('Search…');
minLength = input<number>(2);
autofocus = input<boolean>(false);
search = output<string>();
clear = output<void>();
protected query = signal('');
protected isValid = computed(() => this.query().length >= this.minLength());
protected inputEl = viewChild<ElementRef<HTMLInputElement>>('inputEl');
private destroyRef = inject(DestroyRef);
private debouncer = new Subject<string>();
constructor() {
this.debouncer.pipe(
debounceTime(300),
takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.onSearch());
afterNextRender(() => {
if (this.autofocus()) this.inputEl()?.nativeElement?.focus();
});
}
protected onInput(value: string) {
this.query.set(value);
this.debouncer.next(value);
}
protected onSearch() {
if (this.isValid()) this.search.emit(this.query());
}
protected onClear() {
this.query.set('');
this.clear.emit();
}
}The same component in Rozie
rozie
<!--
SearchInput.rozie
Demonstrates:
- r-model on a form input (sugar for :value + @input)
- $emit for custom events to the parent
- $computed deriving from $data
- $onMount with cleanup return value (Rozie supports the React-style
"return a teardown function from $onMount" pattern as an alternative
to writing a separate $onUnmount)
- .debounce(ms) parameterized modifier on a template event
- Conditional rendering with r-if / r-else
-->
<rozie name="SearchInput">
<props>
{
placeholder: { type: String, default: 'Search…' },
minLength: { type: Number, default: 2 },
autofocus: { type: Boolean, default: false },
}
</props>
<data>
{
query: '',
}
</data>
<script>
const isValid = $computed(() => $data.query.length >= $props.minLength)
const onSearch = () => {
if (isValid) $emit('search', $data.query)
}
const clear = () => {
$data.query = ''
$emit('clear')
}
$onMount(() => {
if ($props.autofocus) $refs.inputEl?.focus()
// Returning a function from $onMount registers a teardown — equivalent to
// a separate $onUnmount, useful when setup and teardown logic belong together.
return () => {
// e.g., abort an in-flight request initialized in this hook
}
})
</script>
<template>
<div class="search-input">
<!--
Modifier on a template event, same grammar as the <listeners> block:
- .debounce(300) waits 300ms after the last keystroke before firing
- .enter triggers immediately on Enter even if the debounce window hasn't elapsed
-->
<input
ref="inputEl"
type="search"
:placeholder="$props.placeholder"
r-model="$data.query"
@input.debounce(300)="onSearch"
@keydown.enter="onSearch"
@keydown.escape="clear"
/>
<button r-if="$data.query.length > 0" class="clear-btn" @click="clear" aria-label="Clear">
×
</button>
<span r-else class="hint">{{ $props.minLength }}+ chars</span>
</div>
</template>
<style>
.search-input { display: inline-flex; align-items: center; gap: 0.25rem; }
input { padding: 0.25rem 0.5rem; }
.clear-btn { background: none; border: none; cursor: pointer; font-size: 1.25rem; }
.hint { color: rgba(0, 0, 0, 0.4); font-size: 0.85em; }
</style>
</rozie>Roughly a third the size, reads top-to-bottom, no decorator soup. The compiler emits an Angular standalone component using the same signal() / input() / output() / viewChild() / inject(DestroyRef) machinery you'd write by hand — see the SearchInput example page for the full Angular output. You don't see it during authoring. You import it normally:
ts
// app.component.ts
import { Component } from '@angular/core';
import SearchInput from './SearchInput.rozie'; // .rozie → standalone component
@Component({
standalone: true,
imports: [SearchInput],
template: `<rozie-search-input (search)="onSearch($event)" />`,
})
export class AppComponent {
onSearch(query: string) { /* … */ }
}The working consumer lives at examples/consumers/angular-analogjs/src/app/AppComponent.ts — it imports the same SearchInput.rozie shown above and runs the component inside a real Angular 19+ Application Builder bundle.
What you don't have to write anymore
Rozie quietly does the Angular ceremony you'd otherwise hand-roll:
| Angular thing | What Rozie handles |
|---|---|
input.required<T>() vs input<T>() | required: true on a <props> member — single source of truth across all six targets. |
output<T>() + emitting | $emit('eventname', payload) |
*ngTemplateOutlet + context-guard ceremony | <slot name="x" :value="…" /> — typed scoped slots with one declaration. |
:host + ::ng-deep for global rules | :root { … } inside <style>. |
@ViewChild capture + a record getter + a [templates] binding | <ng-template [rozieSlot]="expr" let-row="row"> — a single marker directive a contentChildren query collects, for dynamic and non-identifier-named keyed slot fills. |
A hand-written <ng-template let-x> binds Angular's $implicit context key — and a Rozie producer sets $implicit to the whole context object, not a single named value. If you want just one named value out of the context, use the explicit form: let-x="x", not the shorthand let-x.
A keyed [rozieSlot] fill works against any Rozie producer that declares a slot — including one whose slots are all plain static names (<slot name="header">). You never have to reason about how the target producer named its own slots to know whether a keyed fill will land.
The rest of the ceremony delta is cataloged row by row in the creature-comforts matrix: signals lowering (<data> to signal(), $computed to computed(), $watch to effect()), model<T>() two-way binding, the auto-implemented ControlValueAccessor for single-model: true components, hoisted DestroyRef cleanup, $onMount timing via ngAfterViewInit(), <listeners> wiring via Renderer2.listen, and pre-bound slot-context closures.
Incremental adoption
Step 1: Pick the lowest-friction install path
If you're on Angular 19+ with the default Application Builder, you have two options:
Option A — Pre-compile (recommended for first try): Use the Rozie CLI to emit a .ts file you check in. No build-time integration; the output is a normal standalone component.
bash
pnpm add -D @rozie/cli
pnpm rozie build src/app/Counter.rozie --target angular --out src/app/Counter.tsOption B — Build-time integration: If your project already uses the AnalogJS Vite-based Angular toolchain (@analogjs/vite-plugin-angular), drop in @rozie/unplugin/vite. See the Install guide for the workspace setup, including the pnpm packageExtensions patch for analogjs's phantom peer-dependency behavior.
Step 2: Write one component in Rozie
Pick a component that doesn't have hot dependencies — a leaf component like a button, badge, modal, or input. Author it as a .rozie file using the Quick Start template.
Step 3: Import + use it like a regular standalone component
ts
import { Component } from '@angular/core';
import { YourRozieComponent } from './YourRozieComponent'; // .rozie → .ts
@Component({
standalone: true,
imports: [YourRozieComponent],
template: `<rz-your-rozie-component [value]="42" />`,
})
export class HostComponent {}Step 4: Decide if you like it
If the team likes the authoring ergonomics, expand. If not, delete the .rozie source and keep the compiled .ts, a normal Angular standalone component that works on its own — the same zero-lock-in exit the React teams page spells out.
What's idiomatic — what isn't
Native
signal()/computed()/effect()/inject(DestroyRef)input()/input.required()/model()/output()/viewChild()ControlValueAccessorauto-implemented for single-model: truecomponents — your Rozie component is a real Angular form control ([(ngModel)]/formControlNamebind directly)- Standalone components, no NgModule
@for/@ifblock syntax (not*ngFor/*ngIf)Renderer2.listenfor<listeners>block- Strict-templates clean (validated under
ngc --strictTemplatesfor the reference + engine-wrapper examples) - ChangeDetection: signal-driven, no zone.js round-trips for state updates
Documented edges
A handful of small Angular-specific edges (custom modifier value-transforms must be pure expressions; immediate $watch fires before $onMount on Angular and Vue but after on the other targets; TypeScript 5.6+ required) are described in Cross-Framework Parity and Compatibility.
Why Angular shops in particular
Three things make Angular the strongest fit for this pitch:
- The pain delta is the widest. Vue-flavored SFC syntax is the single largest leap from Angular's authoring ergonomics — far more so than from React (which is already JSX-y) or Svelte (also block-based).
- The compiled output is fully native. Rozie emits signals, standalone components, modern block syntax, and
inject(DestroyRef)— the exact Angular that Angular shops are migrating to from older patterns. There is no parallel runtime in the output. - Strict-templates clean. The compiler's output passes
ngc --strictTemplatesfor every reference example. Type-safety doesn't degrade.
When Rozie isn't the right answer for an Angular team
If any of these describes your team, Rozie is the wrong tool:
- You're happy authoring Angular's native template DSL and decorators. The entire pitch is the ergonomics delta. If
*ngFor/@for,[(ngModel)]ceremony, and decorator boilerplate don't register as pain for your team, Rozie is just a compile step that buys you nothing — keep writing Angular. - You ship an Angular-only component library and have no second-framework maintenance burden. You're not the audience; the cross-framework component-library author is — a single-target library gains nothing from a cross-target compiler.
- Your components lean on Angular-specific architecture Rozie deliberately doesn't model — custom structural directives, complex DI provider trees,
HttpInterceptors, route guards, or an NgRx / RxJS-stream data layer. Rozie authors presentational components (props,<data>state, events, two-way binding viamodel()+ CVA, slots). It is not a replacement for Angular's application-architecture layer, and it never tries to be.
Next steps
- Quick Start — write your first
.roziefile. - Adopt incrementally — full per-stack install walkthrough.
- Examples — full source + Angular output for every reference component.
- Compatibility — feature × target matrix.