What is new in Angular v14 - A Game Changer
Discover what is new in Angular 14
Introduction
Angular 14 was recently released with a bunch of exciting features. And today, I'll present them to you. They are:
- Standalone components
- Route Providers
- ENVIRONMENT_INITIALIZER
- Typed Forms
- The
inject()
Function - Setting the Page title from the route
- Autocomplete in the Angular CLI
- Optional injectors in Embedded Views
So let's get started!
Standalone components
For most people, the most significant change in this version is the possibility of creating components without @NgModule
s! Yeah, you got that right.
Before Angular 14
If you are a bit lost, let me show you a folder structure from a classic Angular component:
home
|--home.component.html
|--home.component.css
|--home.component.ts
|--home.module.ts
bashhome
|--home.component.html
|--home.component.css
|--home.component.ts
|--home.module.ts
There, we have an .html
file for the template, a .css
file for the styles, a .ts
file for the component, and another .ts
file for the @NgModule
. This last file imports the dependencies of our component, declares the component, and can also define some providers.
A new possibility with Angular 14
In Angular 14, we can do all that directly in the component, without needing an @NgModule
.
home
|--home.component.html
|--home.component.css
|--home.component.ts
bashhome
|--home.component.html
|--home.component.css
|--home.component.ts
To do that, we just need to set the standalone
property in our component to true
.
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: true,
})
TypeScript@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: true,
})
Be aware!
Be aware:
- Standalone components are not the new way of declaring components! They are another way of declaring components. The classic way of defining
@NgModule
s will not be deprecated.
As a matter of fact. I will still use @NgModule
s instead of standalone components because I like the isolation that @NgModule
s provides.
- Don’t migrate your entire application to standalone components yet! Standalone components are very recent, and it will take us a while to create conventions and define best practices. I recommend waiting a bit longer before jumping ship.
Route Providers
But hey, if we drop the @NgModule
and use standalone components, how can we set provider by route, like we used to do with @NgModule
s?
To address that, Angular added route providers. So, basically, route definitions now have a property called providers
. Allowing us to provide values only to the modules and components rendered by that route.
const NAME = new InjectionToken<string>('token');
export const routes: Routes = [
{
path: '',
component: HomeComponent,
providers: [
{
provide: NAME,
useValue: 'foo'
}
]
}
];
TypeScriptconst NAME = new InjectionToken<string>('token');
export const routes: Routes = [
{
path: '',
component: HomeComponent,
providers: [
{
provide: NAME,
useValue: 'foo'
}
]
}
];
ENVIRONMENT_INITIALIZER
Another thing we used to do with NgModule
s was run a setup script when a lazy loaded module was initialized. In other words, the constructor
of a lazy loaded module would only run when the module was initialized, and some developers take advantage of that to run some kind of setup.
export class HomeModule {
constructor() {
console.log('You can run something here');
}
}
TypeScriptexport class HomeModule {
constructor() {
console.log('You can run something here');
}
}
How can we do that with standalone components? The solution is to use the ENVIRONMENT_INITIALIZER
token.
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: HomeComponent
providers:[
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => console.log("You can run something right here too")
}
]
}
]
TypeScriptconst routes: Routes = [
{
path: '',
pathMatch: 'full',
component: HomeComponent
providers:[
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => console.log("You can run something right here too")
}
]
}
]
This token allows us to provide a setup function that will run before the environment is initialized.
If we want it to run when we navigate to a route, we can provide this token using route providers.
This has the same effect as our previous solution using NgModule
constructor
s and has the added benefit of being more explicit.
Typed Forms
Another long-awaited feature of Angular 14 is typed forms.
Before version 14, form values were typed as any
. This means that we lose all the awesome type-safety of TypeScript.
const control = new FormControl(""),
control.value
//=> control.value: any
TypeScript const control = new FormControl(""),
control.value
//=> control.value: any
By the way, there is a type-safe way of saying that you don't know the type of something. If you're interested in that, check out this one minute video explaining the differences between any
and unknown
.
Anyway, we don't have that problem anymore. Because Angular 14 has strict types for forms. So if a FormControl
deals with a string
, the value will be typed as a string
instead of any
.
const control = new FormControl(""),
control.value
//=> control.value: string | null
TypeScriptconst control = new FormControl(""),
control.value
//=> control.value: string | null
The inject()
Function
Now, this is the most interesting feature to me.
Angular 14 introduces the inject()
function. And it looks very much like React hooks. It gives us a universe of possibilities to reuse our code, the most relevant being that we can now create reusable functions which use dependency injection internally.
export function getPosts() {
const http = inject(HttpClient);
return http.get('/api/getPosts');
}
export class HomeComponent {
readonly posts = getPosts();
}
TypeScriptexport function getPosts() {
const http = inject(HttpClient);
return http.get('/api/getPosts');
}
export class HomeComponent {
readonly posts = getPosts();
}
If you're as interested n functional programming as I am, you know that means a lot! But as Uncle Ben has once said:
This creates implicit dependencies in your functions. I expect my function dependencies to be explicitly declared in the function arguments. So if I see a function with no arguments, I imagine it to have no dependencies. But now, we can create functions that seem pure but inject a lot of dependencies internally, making them harder to test and more coupled to the Angular framework.
A good solution is to follow the same convention that we have on React, which is to prefix those functions with "use"
. So that way, you can easily identify functions that use inject()
internally.
export function useGetPosts() {
const http = inject(HttpClient);
return http.get('/api/getPosts');
}
export class HomeComponent {
readonly posts = useGetPosts();
}
TypeScriptexport function useGetPosts() {
const http = inject(HttpClient);
return http.get('/api/getPosts');
}
export class HomeComponent {
readonly posts = useGetPosts();
}
Set Page title from the route
One common task in web applications is changing the page title on each route.
In Angular, this used to be a very manual process, but now, we can simply define the page title in the route definition.
export const routes: Routes = [
{
path: '',
component: HomeComponent,
title: 'Home',
pathMatch: 'full'
}
];
TypeScriptexport const routes: Routes = [
{
path: '',
component: HomeComponent,
title: 'Home',
pathMatch: 'full'
}
];
But I know what you're thinking:
Don't worry. Angular 14 got you covered.
If you need to customize the title dynamically, you can do so by extending the TitleStrategy
class from the @angular/router
, and providing your new, custom title strategy instead of the default one.
@Injectable({ providedIn: 'root' })
export class PageTitleStrategy extends TitleStrategy {
constructor(@Inject(Title) private title: Title) {
super();
}
override updateTitle(routerState: RouterStateSnapshot) {
const title = this.buildTitle(routerState);
if (title !== undefined) {
this.title.setTitle(`CompanyName | ${title}`);
} else {
this.title.setTitle(`CompanyName | Page without title`);
}
}
}
export const routes: Routes = [
{
path: '',
component: HomeComponent,
title: 'Home',
pathMatch: 'full'
}
];
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(RouterModule.forRoot(routes)),
{ provide: TitleStrategy, useClass: PageTitleStrategy }
]
});
TypeScript@Injectable({ providedIn: 'root' })
export class PageTitleStrategy extends TitleStrategy {
constructor(@Inject(Title) private title: Title) {
super();
}
override updateTitle(routerState: RouterStateSnapshot) {
const title = this.buildTitle(routerState);
if (title !== undefined) {
this.title.setTitle(`CompanyName | ${title}`);
} else {
this.title.setTitle(`CompanyName | Page without title`);
}
}
}
export const routes: Routes = [
{
path: '',
component: HomeComponent,
title: 'Home',
pathMatch: 'full'
}
];
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(RouterModule.forRoot(routes)),
{ provide: TitleStrategy, useClass: PageTitleStrategy }
]
});
Angular CLI Autocomplete
Another nice little thing is that we now have autocomplete in the Angular CLI.
Just type ng
on your terminal and press TAB
to let the Angular CLI either autocomplete your command or give you some help.
> ng se
//=> If you press TAB here, the CLI will complete
> ng serve
TypeScript> ng se
//=> If you press TAB here, the CLI will complete
> ng serve
> ng
//=> If you press TAB here, the CLI will show a list of commands
> ng
add -- Adds support for an external library to your project.
analytics -- Configures the gathering of Angular CLI usage metrics.
build -- Compiles an Angular application or library into an outpu...
cache -- Configure persistent disk cache and retrieve cache sta...
...
TypeScript> ng
//=> If you press TAB here, the CLI will show a list of commands
> ng
add -- Adds support for an external library to your project.
analytics -- Configures the gathering of Angular CLI usage metrics.
build -- Compiles an Angular application or library into an outpu...
cache -- Configure persistent disk cache and retrieve cache sta...
...
Activating the CLI autocomplete
To activate it, you must run the ng completion
command and confirm that you want to enable the autocomplete by typing Yes
.
> ng completion
? Would you like to enable autocompletion? This will set up your terminal so pressing
TAB while typing Angular CLI commands will show possible options and autocomplete arguments.
(Enabling autocompletion will modify configuration files in your home directory.)
> Yes
bash> ng completion
? Would you like to enable autocompletion? This will set up your terminal so pressing
TAB while typing Angular CLI commands will show possible options and autocomplete arguments.
(Enabling autocompletion will modify configuration files in your home directory.)
> Yes
Finally, just restart your terminal to have autocomplete enabled.
Optional injectors in Embedded Views
Last but not least, we can now pass optional injectors in embedded views. In the past, we had to use other APIs to do that, such as the ngComponentOutlet
.
@Directive({ selector: '[foo]' })
export class FooDirective implements OnInit {
constructor(
private vcr: ViewContainerRef,
private templateRef: TemplateRef<unknown>
) {}
ngOnInit(): void {
this.vcr.createEmbeddedView(
this.templateRef,
{}, // context
{
injector: Injector.create({
// pass an injector :)
providers: [
{
provide: 'foo',
useValue: 'bar'
}
]
})
}
);
}
}
TypeScript@Directive({ selector: '[foo]' })
export class FooDirective implements OnInit {
constructor(
private vcr: ViewContainerRef,
private templateRef: TemplateRef<unknown>
) {}
ngOnInit(): void {
this.vcr.createEmbeddedView(
this.templateRef,
{}, // context
{
injector: Injector.create({
// pass an injector :)
providers: [
{
provide: 'foo',
useValue: 'bar'
}
]
})
}
);
}
}
Don't Stop Here
These were the most relevant changes in Angular 14, in my opinion. If you want to see all changes, you can do so by checking out the references in the description.
If you want to dive deeper into the Angular framework, consider
And if your company is looking for remote web developers, consider contacting my team and me. You can do that here.
As always, references are in the description. Like. Subscribe. Have a great day. And I will see you in the next one.
– Lucas
References
- Angular 14 - Academind on Youtube
- O que há de novo no revolucionário Angular 14 - Andrew Rosário on Medium
- Novidades no Angular v14 - Vida Fullstack
- What is new in Angular 14? - Nevzatopcu on Medium
- Angular v14 is now available! - Angular Blog on Medium
- What’s New in Angular v14 - Netanel Basal
- Unleash the Power of DI Functions in Angular - Netanel Basal
- Getting to know the ENVIRONMENT_INITIALIZER Injection Token in Angular - Netanel Basal
- Typed Reactive Forms in Angular — No Longer a Type Dream - Netanel Basal
- Handling Page Titles in Angular - Netanel Basal
- Angular Standalone Components: Welcome to a World Without NgModule - Netanel Basal
- Pass an Injector to Embedded Views - Netanel Basal
- Setting page title from the route module - Brandon Roberts
- Standalone components with Custom Title Strategy in Angular 14