Angular 20: What's New?
New Requirements: TypeScript v5.8 and Node v20
Angular v20 is not ambiguous about its new requirements. You must now be on TypeScript v5.8 (which, by the way, was already supported since v19.2), so it's time to say goodbye to older TypeScript versions.
And for Node, welcome Node v20 as the new minimum. Angular v19 was the farewell tour for Node 18. Time to update those environments, folks!
2025 Style Guide: A Breath of Fresh Air!
The Angular Style Guide has received a major overhaul! Many recommendations have been trimmed down to focus on what really matters.
- Trimmed Filenames: Forget
UserComponent
inuser.component.ts
. It's now justUser
inuser.ts
. Same for directives, pipes, etc. The CLI is already enforcing this for new things. How long will it take for your muscle memory to adapt? - Lighter Folder Structure: The top-level
app
folder might disappear soon (though the CLI hasn't gotten there yet). - Property Visibility:
protected
is now the way for properties used only in your template, andreadonly
for all those Angular-initialized properties (input()
,output()
, etc.). It just makes sense! - Class and Style Bindings: It's official!
[class.something]
and[style.something]
are the recommended champions overngClass
andngStyle
.
This is a big shift. New projects will adopt it by default. Existing ones? Well, you either migrate or stick to the old ways (the CLI helps with a setting for that, phew!).
Signal APIs: Mostly Greenlit and Stable!
Signals are the future, and the APIs are solidifying! Most are now stable:
effect()
toSignal()
toObservable()
afterRenderEffect()
afterNextRender()
linkedSignal()
PendingTasks
Heads up! afterRender()
has been renamed to afterEveryRender()
and is stable. Crucially, the old name is gone with no automatic migration. Oof, that could sting!
Also, TestBed.flushEffects()
(that slightly elusive developer preview API) is deprecated. Use TestBed.tick()
now, which runs the full sync process, much closer to real app behavior. effect()
lost its forceRoot
option (was anyone using it much?), and toSignal()
dropped rejectErrors
(good call, best practices for the win!). pendingUntilEvent()
is still warming up in developer preview.
Zoneless: Out of Experimental, Into Developer Preview!
If signals are the future of reactivity, Zoneless is the future of change detection! It's no longer "experimental," it's now officially in developer preview.
provideExperimentalZonelessChangeDetection
is now justprovideZonelessChangeDetection
.- The CLI flag
--experimental-zoneless
is now just--zoneless
.
The CLI will even ask if you want to enable it for new projects. Ready for #NoZone?
What's Leaving, What's Staying, and What Might Break Your Code:
As with any major version, some things are getting tossed:
- The
ngIf
,ngFor
,ngSwitch
directives are officially deprecated. The built-in control flow (@if
,@for
,@switch
) is king now. Start migrating; they'll likely be gone in v22!ng update
will lend a hand. fixture.autoDetectChanges(boolean)
: The boolean parameter is gone. Just usefixture.autoDetectChanges()
. Usingfixture.autoDetectChanges(false)
in a zoneless test? That will now throw an error.TestBed.get()
: Finally, finally GONE! It was deprecated way back in Angular v9.TestBed.inject()
is your friend (and has been for a while). An automatic migration will handle this.InjectFlags
Enum: Removed. Option objects for DI APIs (likeinject()
) have been the norm since v14.1.DOCUMENT
Token: Moved from@angular/common
to@angular/core
. A migration will update your imports.@angular/platform-browser-dynamic
: Deprecated in favor of@angular/platform-browser
. You'll have to manually update imports for this one for now.@angular/platform-server/testing
: Also deprecated, with no replacement. E2E tests are now the recommended way to verify SSR apps.- HammerJS Integration: Deprecated. HammerJS hasn't seen an update in 8 years. It's time to say goodbye to those framework entities.
ng-reflect-*
Attributes: Gone by default in dev mode. They were for old devtools. If you relied on them (you probably shouldn't have!), you can re-enable them withprovideNgReflectAttributes()
. But maybe... just don't?
Templates: New Tricks Up Their Sleeve!
Your templates just got a bit more expressive:
- Exponentiation:
{{ 2 ** 3 }}
is now possible. Makes calculations easier! - Tagged Template Literals: Yep,
{{ translate\
app.title` }}` is here (technically since v19.2, but it's settled now). void
Operator: Use it like<button (click)="void selectUser()">
to explicitly ignore a function's return, especially useful for event listeners wherereturn false
might prevent default behavior.in
Operator: Check for properties like@if ('invoicing' in permissions)
. Super useful!
Extended Diagnostics: The Compiler Keeps You in the Loop!
More built-in checks to catch common errors:
missingStructuralDirective
: Using something like*ngTemplateOutlet
but forgot to importNgTemplateOutlet
? The compiler will now let you know (withstrictTemplates
).uninvokedTrackFunction
: Wrote@for (user of users; track getUserId)
instead oftrack getUserId(user)
? You'll get a friendly nudge.unparenthesizedNullishCoalescing
: Mixing??
with&&
or||
(e.g.,a ?? b && c
) now requires parentheses for clarity, like{{ a ?? (b && c) }}
. TypeScript does this, so it's great to see in templates too!
You can suppress these in tsconfig.json
if you really want to, but... why would you?
Host Binding Type Checking: No More Mysteries!
There's a new compiler option typeCheckHostBindings
(already in new CLI projects). If you use host metadata in decorators (or @HostBinding
/@HostListener
), the compiler now checks:
- If the binding/listener target (e.g.,
value
in[value]="value()"
) is actually valid for the host element. - If the property in your component/directive class (e.g.,
value()
) actually exists.
This is huge for catching typos and ensuring your host bindings are legit!
Error Handling: Fewer Errors Slipping Through!
provideBrowserGlobalErrorListeners
: A new provider (added by default in new CLI projects) to register global error listeners in the browser. Catches errors Angular might otherwise miss.- Errors in event listeners are now reported to Angular's internal error handler. This means you might see errors in tests that were previously silent. Time to fix them (or use
rethrowApplicationErrors: false
inconfigureTestingModule
as a last resort).
Dynamically Created Components: Level Up!
createComponent()
(and ViewContainerRef.createComponent
) got way cooler in v20! You can now pass options to:
- Specify
directives
to apply to the dynamic component. - Provide input values using the new
inputBinding()
function. - Declare two-way bindings with
twoWayBinding()
. - Listen to outputs with
outputBinding()
.
This is a huge improvement over calling setInput
after the first change detection. More power and control! Will this come to TestBed.createComponent()
too?
Forms: Still Waiting on Signals, but...
No signal-based forms yet (we're all on the edge of our seats for that!). But v20 brings a couple of small but welcome tweaks:
userForm.resetForm(undefined, { emitEvent: false })
: Reset forms without triggering events.markAllAsDirty()
: Finally, a method onAbstractControl
to mark a control and all its descendants asdirty
.markAllAsTouched()
was missing a sibling!
Router: Smoother Navigation Ahead!
Some cool improvements for the router:
- Scroll Options: Pass native scroll options to
ViewportScroller.scrollToAnchor()
/scrollToPosition()
. For example,behavior: 'smooth'
for that slick scrolling effect. - Resolvers with More Context: Child route resolvers can now access resolved data from their parent route!
route.data.user
from the parent will be available. Less gymnastics to get data! - Async Redirects: The
redirectTo
option in route configs can now accept a function that returns a Promise or an Observable for async redirects. (Technically a breaking change due to the evolving return type). - Custom Element Support: Writing Web Components? You can now use a custom element as the host of a
RouterLink
.
Http: Evolving resource
APIs and keepalive
!
resource
API Changes: Thequery
parameter ofresource()
is nowparams
. ForrxResource()
,loader
is nowstream
. Thereload
method moved toWritableResource
(only mutable resources can be reloaded).httpResource
Updates: Themap
option is nowparse
. You can specify the HTTP context in the options. And, the request must now be reactive (e.g.,httpResource<User[]>(() => '/users')
instead of just the URL string).keepalive
Support:HttpClient
now supports thekeepalive
option when using the Fetch API (enabled withwithFetch()
). Requests won't be aborted if the page unloads. Useful for things like analytics.
Profiling: See Where Your App's Performance is Hit!
There's a new enableProfiling()
function in @angular/core
. Call this, and Angular will use the browser's Performance API to label framework operations (change detection, templates, outputs, defer, etc.). Then, open Chrome Devtools, record a performance profile, and see the custom "Angular" track. Finally, a clear view of internal performance!
Devtools: Better Insights!
Angular Devtools are getting smarter:
OnPush
components are now marked as such in the component tree.- Deferred blocks (
defer
) are also displayed. - Signal support is improving – we should see the signal tree soon! Peeking under the hood is now easier.
SSR: Stable APIs and Streamlined Config!
Good news for Server-Side Rendering:
- The
withI18nSupport()
andwithIncrementalHydration()
APIs are now stable! provideServerRendering()
(now in@angular/ssr
instead of@angular/platform-server
) is combined withprovideServerRoutesConfig()
into a singleprovideServerRendering(withRoutes(serverRoutes))
. A migration will handle this.- New CLI apps with
--ssr
get Express v5 and server routing support by default (the--server-routing
option is gone).
Angular CLI: This is HUGE!
The CLI saw a ton of changes. Brace yourselves:
- Updated Naming for 2025 Style Guide: As mentioned,
user.ts
(with classUser
) instead ofuser.component.ts
(classUserComponent
). Same for directives, services. Pipes, resolvers, etc. use hyphens in filenames (from-now-pipe.ts
). A migration forangular.json
configures your existing projects to use the old convention if you want a smooth transition. - TypeScript Config: The
module
option is nowpreserve
(better reflecting modern bundlers).tsconfig.json
uses a "solution" style, referencingtsconfig.app.json
andtsconfig.spec.json
. - Simplified
angular.json
: New projects use@angular/build
directly, dropping@angular-devkit/build-angular
and its transitive Webpack dependencies. That's almost 200 Mb less innode_modules
! Some options likeoutputPath
are also removed as they have sensible defaults. Lighter and more powerful! - Browserslist Config: Now targets the "widely available" base (browsers released < 30 months from the main Baseline set). More consistent and realistic browser support.
- Sass Package Importers: You can now use
pkg:
importers, like@use 'pkg:@angular/material' as mat;
. - Ahead of Time (AoT) Testing and Template Code Coverage: Add
"aot": true
to your test options inangular.json
. Run tests in the same mode as production! Plus, you get code coverage for templates. - Testing with Vitest (Experimental!): Big news! The CLI now supports running tests with Vitest. There's a new
@angular/build:unit-test
builder. Karma and Jasmine might have a new challenger.- Set it up with
"runner": "vitest"
. You'll need a"buildTarget"
. - Defaults to Node with
jsdom
(install it). - Update
tsconfig.spec.json
types
to["vitest/globals"]
. - Browser Mode: Yep!
"browsers": ["chromium"]
(or firefox, webkit). Uses Playwright or WebdriverIO (install one). - Watch mode is faster (only runs affected tests). Full runs might be a bit slower than Karma.
--no-watch
is often implied in CI.- New
--debug
flag (Vitest + jsdom/Playwright). - Limitations: No custom Vitest config file yet. But the
providersFile
option lets you set up things like zoneless testing. Reporter and coverage exclusions are inangular.json
.
- Set it up with
- Automatic Chrome Workspace Folders: The Vite dev server now helps Chrome Devtools map your project files, allowing direct editing from Devtools that saves to your disk! Enable it in Chrome flags.
- Sourcemaps without Sources: Generate sourcemaps without embedding the original source code (
"sourcesContent": false
). Great for production error reporting without exposing all your code.
Angular and its Future with AI!
Angular is positioning itself to facilitate the development of applications with generative AI capabilities:
File: llms.txt
An llms.txt
file is maintained in the Angular repository. This file helps large language models (LLMs) discover the latest and most correct Angular documentation and code examples, so they generate more modern and accurate code, avoiding issues with obsolete APIs or syntax.
Guides and Resources: Guides and resources are being provided, angular.dev/ai, including examples and live streams showing how to integrate Angular with tools like Genkit and Vertex AI from Google Cloud.
What We Might See in Angular v21
- Selectorless Components: Imagine this:
Components used by their class name directly in templates! Directives might use animport { User } from './user/user'; // TS import is still needed @Component({ template: '<User [name]="name()" (selected)="selectUser()" />', // NO Angular 'imports' array needed for User! }) export class App { /* ... */ }
@
prefix (e.g.,<User @CdkDrag />
). Pipes also by class name ({{ date | FromNowPipe }}
). The syntax is FAR from final, but the compiler work has begun. Mind-blowing, right? This could be revolutionary! An RFC should be coming. - Signal-based Forms (The Third Way?): Get ready for a potential new way of doing forms, separate from template-driven and reactive!
A new// VERY EARLY PROTOTYPE - DO NOT USE YET! @Component({ selector: "user-form", imports: [FieldDirective], // New directive template: ` <form> <label>Username: <input [field]="userForm.username" /></label> <label>Name: <input [field]="userForm.name" /></label> </form> `, }) class UserFormComponent { userModel = signal<UserModel>({ /* ... */ }); protected readonly userForm: Field<User> = form<User>( // new form() function userModel, // data to edit (userPath: FieldPath<User>) => { // schema for dynamic behavior and validation disabled(userPath.username, () => true, "Cannot change username"); required(userPath.name); error(userPath.age, ({ value }) => value() < 18, "Must be over 18"); } ); }
form()
function andField
class to handle form state with signals. Check out the design doc if you're curious, or wait for the RFC!