Angular 17-19 standalone components, signals, control flow, dependency injection patterns
Quick Guide: Components are standalone and declare their own
imports; NgModules are opt-in. State is signals —signal(),computed(),linkedSignal()for derived state you also write to,effect()only for genuine side effects andafterRenderEffect()for DOM work. Communication isinput(),output()andmodel(). Templates use@if,@for(always withtrack),@switchand@defer, none of which need an import. Dependencies come frominject().
Detailed Resources:
inject(), InjectionToken, injection optionsmodel() two-way binding, and when input() + output() is the better fit@defer trigger, with prefetch and placeholder timinglinkedSignal(), resource(), rxResource(), afterRenderEffect() phasestoSignal(), toObservable(), and which of the two a problem wantsstandalone: true is the default, so the flag is noise; write standalone: false only for a component that genuinely belongs to an NgModule.standalone: true is stated explicitly on every component, directive and pipe.linkedSignal() and afterRenderEffect() land in 19, httpResource() in 19.2, and the resource family is still experimental. examples/angular-19-features.md marks each one.<critical_requirements>
Declare inputs and outputs with input(), output() and model(). They are signals, so a computed() can depend on an input directly and no ngOnChanges is needed to notice it changed.
Take dependencies with inject() in a field initialiser. It works outside a constructor — in a function, a route guard, a factory — where constructor parameters cannot reach.
Write templates with @if, @for, @switch and @defer. They need no import, narrow types better than the structural directives, and @for has @empty and the $index/$first/$last context built in.
Give every @for a track expression. Without a stable key Angular rebuilds the rows rather than moving them, which loses focus and element state along with the performance.
Update a signal through .set() or .update() returning a new reference. Equality is Object.is, so mutating the array or object in place leaves the reference unchanged and nothing is notified.
Use linkedSignal() for derived state that is also writable. It recomputes from its source and still accepts a direct write, which is what a pair of signals kept in step by an effect() was imitating.
</critical_requirements>
Auto-detection: Angular standalone component, signal, computed, effect, linkedSignal, resource, rxResource, httpResource, input(), output(), model(), @if, @for, @switch, @defer, inject(), InjectionToken, provideRouter, bootstrapApplication, afterRenderEffect, afterNextRender, DestroyRef, toSignal, toObservable, viewChild, viewChildren
Applies to:
imports array and their providerssignal, computed, linkedSignal, effect, afterRenderEffectinput, output and model@defer lazy loadinginject() and injection tokensHandled elsewhere:
styles or styleUrl and settles nothing about what goes in themStandalone removed the second declaration site. A component names what it uses in its own imports, so the dependency graph is readable from the component and a lazy route can point at a component rather than at a module wrapping one.
Signals then removed the second question. Change detection used to ask "what might have changed?" and walk the tree; a signal records who read it, so an update notifies exactly those consumers. That is why the guidance keeps pushing work down the chain: computed() where a value is derived, linkedSignal() where it is derived and writable, effect() only where something outside the graph has to happen — and afterRenderEffect() where that something is the DOM, because it runs in phases that keep reads and writes from thrashing layout.
A component declares its own imports and communicates through signal functions.
@Component({
selector: "app-user-card",
imports: [DatePipe],
template: `
<h2>{{ user().name }}</h2>
<time>{{ user().createdAt | date: "mediumDate" }}</time>
<button (click)="edit.emit(user())">Edit</button>
`,
})
export class UserCardComponent {
user = input.required<User>();
edit = output<User>();
}
Full code: examples/core.md
count = signal(0);
doubleCount = computed(() => this.count() * 2);
this.count.set(5);
this.count.update((value) => value + 1);
items = signal<Item[]>([]);
this.items.update((items) => [...items, newItem]);
computed() is memoised and lazy; a method with the same body recomputes on every template read. Reserve effect() for logging, analytics, storage and other work outside the signal graph.
Full code: examples/core.md
options = input.required<Option[]>();
selected = linkedSignal(() => this.options()[0]);
selected follows options and still accepts selected.set(...) from a click. Its computation form takes the previous value, which is how a selection survives a source change instead of resetting.
Full code: examples/angular-19-features.md
placeholder = input("Search...");
minLength = input.required<number>();
query = model("");
search = output<string>();
isValidSearch = computed(() => this.query().length >= this.minLength());
model() gives the parent [(query)]. Reach for it where the child genuinely owns the edit; input() plus output() keeps the flow one-way and is the better default.
Full code: examples/model.md
@switch (state()) { @case ("loading") {
<div>Loading…</div>
} @case ("error") { <button (click)="retry.emit()">Retry</button> } @case
("success") { @for (user of users(); track user.id; let i = $index) {
<li>{{ i + 1 }}. {{ user.name }}</li>
} @empty {
<li>No users found</li>
} } }
@if (user(); as user) binds the narrowed value, so the signal is called once rather than in every expression beneath it.
Full code: examples/core.md
@defer (on viewport) {
<app-heavy-chart />
} @placeholder (minimum 200ms) {
<div class="chart-skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
<div class="spinner"></div>
} @error {
<div>Failed to load chart</div>
}
@placeholder reserves the space, and the after/minimum timings on @loading are what stop a fast load flashing a spinner. Defer what is below the fold, behind an interaction, or conditional — never what is visible on arrival, which trades bundle size for LCP.
Full code: examples/defer.md
@Injectable({ providedIn: "root" })
export class UserService {
private http = inject(HttpClient);
private config = inject(CONFIG_TOKEN, { optional: true });
}
inject() also takes { skipSelf: true } to start at the parent injector and { self: true } to refuse to leave the current one. It must run in an injection context — a field initialiser or a constructor — never inside a method.
Full code: examples/dependency-injection.md
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(),
withPreloading(PreloadAllModules),
),
provideHttpClient(),
],
};
bootstrapApplication(AppComponent, appConfig);
export const routes: Routes = [
{
path: "users/:id",
loadComponent: () =>
import("./users/user-detail.component").then(
(m) => m.UserDetailComponent,
),
},
];
loadComponent lazy-loads a component with no wrapper module. withComponentInputBinding() binds route and query params straight to input() signals, so a route component needs no ActivatedRoute — a query param that may be absent is typed input<string | undefined>().
userResource = resource({
params: () => ({ id: this.userId() }),
loader: async ({ params, abortSignal }) => {
const response = await fetch(`/api/users/${params.id}`, {
signal: abortSignal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return (await response.json()) as User;
},
});
The resource re-runs when params changes and aborts the superseded request, so the signal-plus-effect combination that used to race is not needed. Guard reads with hasValue(), which narrows the type as well as the state. rxResource() takes an observable loader and httpResource() (19.2) goes through HttpClient and its interceptors.
Full code: examples/angular-19-features.md
private destroyRef = inject(DestroyRef);
private elementRef = inject(ElementRef);
width = signal(0);
constructor() {
afterNextRender(() => {
const observer = new ResizeObserver(([entry]) => this.width.set(entry.contentRect.width));
observer.observe(this.elementRef.nativeElement);
this.destroyRef.onDestroy(() => observer.disconnect());
});
}
| Legacy hook | Signal-era replacement |
| -------------------- | -------------------------------------------- |
| ngOnInit | field initialiser, or effect() |
| ngOnChanges | effect() reading the input() signal |
| ngAfterViewInit | afterNextRender() |
| ngAfterViewChecked | afterRender() (afterEveryRender() in 20) |
| ngOnDestroy | DestroyRef.onDestroy() |
| DOM side effects | afterRenderEffect() with explicit phases |
Full code: examples/angular-19-features.md
users = toSignal(this.userService.getUsers(), { initialValue: [] });
count$ = toObservable(this.count);
toSignal() needs an initialValue for any source that has not emitted yet; without one the signal's type includes undefined and a template read before the first emission throws.
Full code: examples/rxjs.md
</patterns><red_flags>
Breaks at runtime:
items().push(x) leaves the reference identical, so Object.is reports no change and nothing re-rendersinject() called from a method — it needs an injection context, and throws outside onetoSignal() without initialValue on a source that has not emitted — reads before the first emission failresource(), rxResource() or httpResource() used for a write — all three are read-only; a POST, PUT or DELETE goes through HttpClientresource.value() read without checking hasValue() — the guard is what narrows away the loading and error states@for without track — Angular tears down and rebuilds each row, discarding focus, scroll position and animation stateeffect() — the return value is ignored, so a timer or subscription opened there leaks on every re-run; teardown goes in the onCleanup callback the effect body is handed as its argumentSurprising behaviour:
standalone: true is the default from 19, so writing it is harmless noise while writing nothing is correct — the two look identical in review@defer renders its @placeholder during server rendering and ignores every trigger therelinkedSignal() resets to its computed value whenever the source changes; preserving a user's choice needs the computation form that receives the previous valueallowSignalWrites was removed in 19 and writing a signal inside an effect() is now allowed — which makes an effect-maintained derived value compile quietly where it used to complainafterRenderEffect() defaults to the mixedReadWrite phase, the one that thrashes layout; name earlyRead and write insteadsignal() compares with Object.is, so two structurally equal objects count as a change unless a custom equal is supplied</red_flags>
npx skills add agents-inc/web-framework-angular-standalone下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer