What I have changed in my coding approach
From v14, The Angular team has certainly been busy. I work with Angular from the beginning of Angular 2. And I have never seen so many features in such a short amount of time.
My last article was edited 7 months ago and it’s time to share with you how I have changed my daily coding approach since v14.
In some cases, it will be a before / after comparison with the feature to show you quickly the benefits. It will allow you to understand how to update your code with examples.
Angular v14
Standalone components
Before
// my.component.ts
@Component({
...
})
export class MyComponent {}
// my.module.ts
@NgModule({
declarations: [MyComponent],
exports: [MyComponent],
imports: [AnotherStuff]
})
export class MyModule {}
Now
// my.component.ts
@Component({
standalone: true,
imports: [AnotherStuff]
...
})
export class MyComponent {}
Advantage
One less file to maintain and less code. Be careful, the standalone component must be imported .
this can also be applied to the AppComponent, you “just” have to te rewrite your main.ts as follow:
bootstrapApplication(AppComponent, {
...
importProvidersFrom(/* the imported modules in the app.module file*/),
...
});
Now there are many features you can provide without module, think to read the docs.
Typed Angular Forms
So highly anticipated and at the same time, somewhat disappointing for me.
Before ,
we only worked with an any type for all of our forms. It was a nightmare to maintain and to evolve.
const form: FormGroup = new FormGroup({
lastName: new FormControl(''),
firstName: new FormControl(''),
});
form.setValue({ ... }); // no type check
form.controls.(...); // no autocompletion
form.controls.firstName.setValue(0); // no type check every thing is ok...
Now ,
all of these points are resolved but there is still a problem. I find the syntax a bit indigest.
type FormType = { lastName: FormControl<string>; firstName: FormControl<string> };
const form: FormGroup<FormType> = new FormGroup<FormType>({
lastName: new FormControl(''),
firstName: new FormControl(''),
});
So why I find it indigest? Because our data models are not defined with Form[Control|Group|Array] types. it’s more like:
export interface Data {
lastName: string; // no FormControl
firstName: string; // no FormControl
}
So how to use the Data model with our FormGroup? Until now, no native solution, we have to use som hack.
I choose to use an intermediate type:
export type FormTyped<T> = {
[Key in keyof T]: FormControl<T[Key]>;
};
It does not handle sub FormGroup or FormArray but in most of my cases it does the work. If you have any suggestion to improve this, don’t hezitate let me a comment.
Advantage
Type checking, autocompletion.
If you want / need to stay like in the old way you can use the UntypedForm[Control|Group|Array|Builder].
Bind to protected component members
Before ,
only public properties could be accessible from the HTML template. It has often been a bit frustrating for me to make some properties public just because of the template.
@Component({
template: `{{ myProperty }}`
})
class Component {
public myProperty?: string;
}
Now ,
we can really choose what to publicly expose from our component:
@Component({
template: `{{ myProperty }}`
})
class Component {
protected myProperty?: string;
}
Angular v15
RouterTestingHarness
Since RouterTestingHarness has come, I find it easier to test routing, check guards calls and co.
@Component({standalone: true, template: 'hello {{name}}'})
class TestCmp {
name = 'world';
}
it('navigates to routed component', async () => {
TestBed.configureTestingModule({
providers: [provideRouter([{path: '', component: TestCmp}])],
});
const harness = await RouterTestingHarness.create();
const activatedComponent = await harness.navigateByUrl('/', TestCmp);
expect(activatedComponent).toBeInstanceOf(TestCmp);
});
Advantage
I use a specific routes.spec.ts file for only testing routing. It makes tests more readable.
Before, I often have seen projects without routing tests. Now, there is no excuse.
Simplify routes files and use Router API
I don’t want to rewrite an app-routing.module.ts… I know you know what I mean. But here how to do now, bonus it’s a lazy load example.
// app.routes.ts
export const appRoutes: Routes = [{
path: 'lazy',
loadChildren: () => import('./lazy/lazy.routes')
.then(routes => routes.lazyRoutes)
}];
// lazy.routes.ts
import {Routes} from '@angular/router';
import {LazyComponent} from './lazy.component';
export const lazyRoutes: Routes = [{path: '', component: LazyComponent}];
// main.tous
bootstrapApplication(AppComponent, {
providers: [
provideRouter(appRoutes)
]
});
Directive composition API
I have written two articles about it:
Functional router guards
Before ,
we have to define our guard in a specific file like below:
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router){};
canActivate(...): boolean | UrlTree {
let isLoggedIn = this.authService.isAuthenticated();
if (isLoggedIn) return true
return this.router.createUrlTree(['/contact']);
}
Now ,
you simply can do it in your routes file:
export const routes = [{
...
canActivate: [
(): boolean | UrlTree => {
const authService = inject(AuthService);
if (authService.isAuthenticated()) return true;
return inject(Router).createUrlTree(['/contact']);
},
],
}];
Or you can extract it into a plain function, it depends on your needs:
function isAuthenticated(): boolean | UrlTree {
const authService = inject(AuthService);
if (authService.isAuthenticated()) return true;
return inject(Router).createUrlTree(['/contact']);
};
export const routes = [{
...
canActivate: [isAuthenticated],
}];
Advantage
More readable, less code. Just what we love.
Self-closing tags
It allows to streamline the HTML code.
<!-- before -->
<app-my-component></app-my-component>
<!-- now -->
<app-my-component/>
Angular v16
Required inputs
How to handle required inputs before Angular v16? You will find the answer in one of my previous article:
How to Handle Required Inputs in Angular
Since v16, no hack is needed, even custom decorators are deprecated, you just have to do this:
@Component({ ... })
class MyComponent {
@Input({ required: true }) myProp!: string;
}
Passing router data as component inputs
I think this is one of my favourite feature. Why? Because we don’t need to have the ActivatedRoute dependency in our component, we don’t need to handle static or resolved route data, path parameters, matrix parameters, and query parameters no more.
Before
private readonly activatedRoute = inject(ActivatedRoute);
private hero$: Observable<Hero> = this.activatedRoute.queryParams.pipe(
switchMap(({ heroId }: Params) => this.service.getHero(heroId)),
);
Now
In your component you just have to define the prop with the @Input decorator:
@Input()
set id(heroId: string) {
this.hero$ = this.service.getHero(heroId);
}
Don’t forget to add this in your routing config:
providers: [
provideRouter(appRoutes, withComponentInputBinding()),
]
Advantage
Your component is more generic and can be used in all cases.
Conclusion
I don’t talk about signal here. It’s a great feature but for me it will be very interesting from the v16.1 and the add of the transform feature for Inputs.
As we can see, many features to make Angular easier, more accessible and more readable. But yes, I’m like you I’m waiting for all the signals feature. It seems we have to wait until the v17.
If you need more details about a feature let me a comment, don’t hezitate.
Thanks for reading.
Top comments (2)
Hello how are you?! I hope you're okay!
Interesting article! I have a tip for you and your articles!
In your code blocks you can specify what is the language of the code, try to do below like in the picture:
Thank you very much. Hmm it’s an import from medium but okay I will take that into account 👍🏼