I love Vue.js but learning another JS framework could be useful in the missions I participate in.
I decided to deepen my knowledge of Angular with a 4h YouTube course.
The course is based on Angular 17, but I worked with Angular 19 at the time I followed it, so I became acquainted with the more recent API that Angular provides.
Installation of the development environment
Install NodeJS
For Windows, use Scoop:
|
|
Note: Install the LTS version to avoid “Warning: The current version of Node (23.9.0) isn’t supported by Angular.” messages from Angular on the next step.
Install Angular
|
|
IMPORTANT: At the time of writing this, Angular is at v19 while the course was taught in Angular 17 and 18.
Extensions for VSCode
I personally use the following:
- https://marketplace.visualstudio.com/items?itemName=1tontech.angular-material
- https://marketplace.visualstudio.com/items?itemName=alexiv.vscode-angular2-files
- https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss
- https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense
- https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics
- https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
- https://marketplace.visualstudio.com/items?itemName=editorconfig.editorconfig
- https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-close-tag
- https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag
- https://marketplace.visualstudio.com/items?itemName=gruntfuggly.todo-tree
- https://marketplace.visualstudio.com/items?itemName=humao.rest-client
- https://marketplace.visualstudio.com/items?itemName=infinity1207.angular2-switcher
- https://marketplace.visualstudio.com/items?itemName=john-crowson.angular-file-changer
- https://marketplace.visualstudio.com/items?itemName=loiane.angular-extension-pack
- https://marketplace.visualstudio.com/items?itemName=obenjiro.arrr
- https://marketplace.visualstudio.com/items?itemName=patbenatar.advanced-new-file
- https://marketplace.visualstudio.com/items?itemName=pucelle.vscode-css-navigation
- https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype
- https://marketplace.visualstudio.com/items?itemName=rctay.karma-problem-matcher
- https://marketplace.visualstudio.com/items?itemName=segerdekort.angular-cli
- https://marketplace.visualstudio.com/items?itemName=simontest.simontest
- https://marketplace.visualstudio.com/items?itemName=steoates.autoimport
- https://marketplace.visualstudio.com/items?itemName=stringham.move-ts
- https://marketplace.visualstudio.com/items?itemName=techer.open-in-browser
- https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost
Create a project
|
|
See my notes about the discountinued assets folder under src.
The components
To create a component, use this command:
|
|
The above creates a new subfolder components/playing-card under app. The scaffolded component is defined with the .css, .html, ts and spec.ts files.
To skip the test file generation, simply the flag --skip-tests on the above command.
Inputs And Signal Inputs
The first manner to declare inputs is the decorator:
|
|
You can then use it in the parent component that uses it:
|
|
However I learned that if you define an input with a different type than a string, then you must use the [ ].
For example:
|
|
It can also contain a simple JavaScript expression:
|
|
Similarly, we can pass on objects:
|
|
To work, you should use the best practice to create a model class that you use to initialize pik and set the input type of app-playing-card to the class name.
For example, I could have this class:
|
|
I use it to declare pik in my app.component:
|
|
About `!`
The ! is the definite assignment assertion operator.
It tells TypeScript’s compiler: “I know this property looks like not initialized, but trust me—it will be assigned a value before it’s used.”
Without it, TypeScript would error because the object pik is declared but not immediately initialized at the declaration. Since you’re assigning it to the constructor, the ! suppresses that error.
And I use it to type the input of my child’s component:
|
|
Another feature using the @Input decorator is its configuration: you could make the card input required:
|
|
And TypeScript would tell you the following:
You can customize the attribute name with alias or transform your input object into transform, but I don’t see a good use case to give an example for it.
Now, since Angular 17, you can use the signal inputs the following way:
|
|
In the HTML, you’ll need to add () to access the input properties.
|
|
Outputs, signal outputs and models
When we need to communicate data from a child component to its parent, we can use the @Output decorator. We also call it emitted events.
A very simple example would look like this:
|
|
In your child’s component HTML file, you can add the searchClick to a button:
|
|
Then, on your parent component TS file, you add a property searchClickCount initialized to 0 and you can listen to events and print out the updated searchClickCount value.
|
|
Now, often, we pass data up and in the above example, we don’t.
Let’s say we want to show the searched term on the parent component.
First we need to update the child component TS file:
|
|
Then we update the child component HTML file to use searchTerm with the directive [ngModel]:
|
|
Finally, we add the searchedTerm = $event property to the parent component to complete the two-ways data binding and we finish with updating the parent component HTML file:
|
|
However, we can shorten that code to the following:
|
|
This is only possible because the output in the child component is named like the input with the prefix Change. Angular will tell you if you use the short version incorrectly.
Similarly to inputs, from Angular 17.3, you can use the new method with output signals:
|
|
All the rest of the code doesn’t change.
Since Angular 17.2, you could also simplify ever more the code using model. In your TS file, the child component would become:
|
|
In the HTML, you can change the code to the following thanks to the two-ways data binding with [(ngModel)] directive:
|
|
With that, you can remove the updateSearch method.
Detecting changes
Angular has several lifecycle hooks on components that help you to initialize a component and execute logic on changes.
You can find the full list in the documentation.
Default Strategy vs OnPush
By default, when a event is triggered on a component, Angular checks all the component tree.
This could causes performance issue on a large application.
That when you can use the OnPush strategy. This limits Angular to only check OnPush components when their inputs change, an event occurs within them, or a bound observable emits — skipping unnecessary checks otherwise.
Sergio, author of the course I followed, explains it very well in his course. If you don’t speak french, enable the auto-translate subtitles on YouTube.
Signals In Angular
Coming from Vue.js, I feel like that Angular took the best of Vue’s reactivity with the signal primitives that you can use from Angular 16:
signal()≈ref()in Vue, both create reactive primitives.computed(() => {}): is it a copy-paste from Vue? At least, it works the same.effect()≈watchEffect()(not watch) which runs automatically when dependencies change
For example, let’s say we store a selected index into a signal and an object into a computed that changes as the index changes:
|
|
Then, in the HTML, we can change:
|
|
to
|
|
Using signals makes the code cleaner and shorter. For example, the TypeScript was the following using the historic method:
|
|
With signals and keeping the same functionnality, we have:
|
|
Why Signals Then
Eventually, Angular will drop out the change detection, performed currently with the library zone.js, and use signals to check only the components that we need on events.
This will improve greatly the performance of large applications!
Loops and conditions
Using Loops
Nothing extraordinary for simple usage. Make sure to import CommonModule in your TS file and then use the *ngFor directive as follows:
|
|
Using Conditions
With conditions, we can use different approaches:
-
several
*ngIfdirectives1 2 3 4 5 6 7<p *ngIf="filteredCards().length === 0" style="text-align: center"> No card found </p> <p *ngIf="filteredCards().length > 0" style="text-align: center"> Found {{ filteredCards().length }} card{{ filteredCards().length > 1 ? "s" : "" }}! </p> -
a single
*ngIfwith a<ng-template>element. It makes me think named slots, but it isn’t the same.1 2 3 4 5 6 7 8 9 10 11 12<p *ngIf="filteredCards().length === 0; else resultsFound" style="text-align: center" > No cards found </p> <ng-template #resultsFound> <p style="text-align: center"> Found {{ filteredCards().length }} card{{ filteredCards().length > 1 ? "s" : "" }}! </p> </ng-template> -
a single
*ngIfwith several<ng-template>elements1 2 3 4 5 6 7 8 9 10 11 12 13<p *ngIf="filteredCards().length === 0; then empty; else resultsFound" style="text-align: center" ></p> <ng-template #empty> <p style="text-align: center">No cards found</p> </ng-template> <ng-template #resultsFound> <p style="text-align: center"> Found {{ filteredCards().length }} card{{ filteredCards().length > 1 ? "s" : "" }}! </p> </ng-template>
New Syntax For Conditions
With Angular 17, new syntax brought the ability to write cleaner and clearer code.
If we take the last example above with the condition, we could now write:
|
|
Similarly, we rewrite the loop:
|
|
What does the track mean? Angular uses it to track DOM updates to the minimum when the data changes.
Regarding @for, you can use it alongside with @empty’ so that our previous code the @if…@else` becomes:
|
|
@for provides a few extra variables you can use: $index, $first, $last, $odd, $event and $count. Read the documentation for more details.
If you were to code nested @for, accessing those built-in variables could become tricky. You can name each variable for a @for after the track like so:
|
|
The same exists on *ngFor but you must declare a local variable if you need to use them. Please read the documentation for more information on the topic.
Services
A service in Angular allows to separate the UI logic from the data and business logic.
We use services as injectable singletons.
You can create the service using the CLI:
|
|
Before Angular 14, you could inject services into components through the constructors, hence the name Constructor Dependency Injection that many software engineers use when implementing S.O.L.I.D principles.
|
|
However, that method makes inheritance complex. Instead, you can now use the new method inject:
|
|
Angular’s services bridge the components to the data source, whatever it could be for your application.
It can contain CRUD methods or any business logic to prepare data for your components.
For example:
|
|
Routes
It felt very similar to Vue Router I use in Vue, though since I followed the 2024 Masterclass of VueSchool, I like the Nuxt approach with Unplugin Vue Router that uses file-based routing.
Adding a Route
The application creation process provides you with a app.routes.ts file that remains empty by default.
You add a route with the following:
|
|
Then you add to import: [] on the app.component.ts the RouterOutlet to add it to the app.component.html:
|
|
If you need to redirect a path to another, let’s say / to /home, you can add another route like so:
|
|
What about handling routes not found
|
|
However, just like with Vue Router, the order matters so put this route to the bottom of the list… 😁
Handling Parameters on a Route
Again, very similar to Vue Router:
|
|
But what if you have similar routes? For example, the one above and this one below:
|
|
Well, you can group them in the following manner:
|
|
How Do You Read Route Parameters
Given the /card/:id route defined previously, you need to load the current route by injecting the ActivatedRoute into the target CardComponent component:
|
|
With that route variable, you can parse from the params property the id:
|
|
You can then use the cardId signal in the HTML file.
How Do You Navigate to a Route
Let’s take an example with a “Next” navigation button on the /card/:id route. We want to increment the cardId on each click.
The next() method that we’ll use in the HTML file would like this:
|
|
But… you may notice an issue. When you click the first next, the route changes, but not the HTML. And if you click again, nothing changes.
Why?
Because we use a snapshot of the params and it isn’t subscribed to. Also, Angular doesn’t execute the ngOnInit again. Therefore, cardId doesn’t get updated.
To solve this, I hinted at the solution: we need to subscribe (topic detailed in more depth below) to the route’s params change.
Though, I’ll provide the solution in the following code snippet:
|
|
With that code, the HTML gets updated and you can click next infinitely.
Reactive forms
We have several types of form handling in Angular:
- Template Driven Forms: With this method, we use the two-binding with
ngModelin the HTML file. - Reactive Forms: With this method, the behavior of the form is declared in the TS file.
Let’s look at the Reactive Form method.
The Basics
To start, we need to add the ReactiveFormsModule module to the TS file to start adding a new FormControl that represents the individual inputs:
|
|
The default is required and you can add validators using Validators class provided by Angular Forms.
While we’re in the TS file, let’s add the submit method that receives the data submitted:
|
|
Now, in the HTML file, we can declare the new form:
|
|
In the code above, from bottom to top:
- the submit button is disabled as long as the two fields aren’t valid.
- on each field, we bind the form control to the input
- the form element binds the submit method to the submit event.
You probably want to tell me that it isn’t practical to check each field on the submit button. That’s where FormGroup comes into the scene!
FormGroup Usage
For the sake of making the article short, I’ll limit the example to two fields:
-
In the TS file, you define the form group:
1 2 3 4 5 6 7 8form = new FormGroup({ name: new FormControl("", [Validators.required]), hp: new FormControl(0, [ Validators.required, Validators.min(1), Validators.max(200), ]), }); -
In the HTML file, you need to adapt a few things:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21<!-- first, add `[formGroup]="form"` to the form element --> <form [formGroup]="form" (submit)="submit($event)"> <div class="form-field"> <label for="name">Name</label> <!-- next, change `[formControl]` to `formControlName` --> <input id="name" name="name" type="text" formControlName="name" /> <!-- the method `isFieldValid` groups the logic of the previous to avoid repetition --> @if (isFieldValid("name")) { <p class="error">This field is required</p> } </div> <div class="form-field"> <label for="hp">HP</label> <input id="hp" name="hp" type="number" formControlName="hp" /> @if (isFieldValid("hp")) { <p class="error">This field is invalid</p> } </div> <!-- finally, we can use `form.invalid` to check form's validity --> <button type="submit" [disabled]="form.invalid">Save</button> </form>
FormBuilder Usage
To simplify the TypeScript code, we could also use the FormBuilder service.
For that, let’s import it first:
|
|
Then, you can refractor the form group as follows:
|
|
Build Select Input
If you need a drop-down input with a select element, you need to:
-
declare the source of data
-
add the
selectelement with afor loopto add the options1 2 3 4 5 6<label for="type">Type</label> <select id="type" name="type" type="number" formControlName="type"> @for (type of cardTypes; track type) { <option [value]="type.Id">{{ type.Name }}</option> } </select>
Handling File Input
In the case of a file input, you’ll need to handle the file change with a custom method. The HTML listens to the (change) event:
|
|
And the onFileChange takes care of updating the target form field:
|
|
Handle Multiple Validators
The simple way to achieve this could happen with the following:
|
|
Angular Material
Angular Material is Google’s official UI component library for Angular that implements Material Design principles.
You can install it with the following command:
|
|
And you’ll need to answer some questions:
|
|
Then, head to Angular Material website to make your pick.
In the example so far, we can update the text inputs, the number inputs and the select input.
To do so, we need:
-
to import the modules necessary:
1 2 3 4 5 6 7 8imports: [ /* The existing modules or components */ /* ... */ /* And add the following for Material compenents */ MatButtonModule, MatInputModule, MatSelectModule, ], -
to implement the modules into the HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25<!-- for the text and number inputs --> <mat-form-field> <!-- replaces the `<div class="form-field">` --> <mat-label for="name">Name</mat-label ><!-- replaces the `<label>` --> <!-- add the `matInput` attribute --> <input matInput id="name" name="name" type="text" formControlName="name" /> @if (isFieldValid("name")) { <!-- replaces the `<p class="error">` --> <mat-error>This field is required</mat-error> } </mat-form-field> <mat-form-field> <mat-label for="type">Type</mat-label> <!-- replaces the `<select>` --> <mat-select id="type" name="type" type="number" formControlName="type"> @for (type of cardTypes; track type) { <!-- replaces the `<option>` --> <mat-option [value]="type">{{ type }}</mat-option> } </mat-select> @if (isFieldValid("type")) { <mat-error>This field is invalid</mat-error> } </mat-form-field>
Now, Angular Material doesn’t provide any components for file input. Sergio takes the smart option to show a button to simulate the click on “Choose file” while hiding the input of type file.
|
|
Then, we implement the method update the button:
|
|
Authentication management
Introduction
We need to start by adding a provider, since the authentication will require using a REST API through an HTTP client.
To perform this, let’s add that provider to app.config.ts:
|
|
Create the AuthService
Next step, you create a new AuthService and you import the HttpClient and use it in your new service:
|
|
Next, we add the user property to the service that will be a signal of type User | null | undefined.
|
|
In authentication, we usually need:
- a method
loginthat receives the credentials. - a method
logoutthat terminates the session. - a method
getUserthat retrieves the user’s information.
Let’s code their signature:
|
|
We need to add the interface ICredentials:
|
|
And the model User :
|
|
We continue with the call to the login method of the API. Sergio’s API uses a login method that returns a token that we need to store in local storage so we can use it later.
|
|
Why are there three types of login and getUser methods?
undefinedidentifies the use case “We don’t know yet if user is logged in”.nullidentifies the use case “We know the user isn’t logged in”.Useridentifies the use case “User is logged in”.
Now, here is the implementation of the getUser and logout methods:
|
|
PS: The tap method is a RxJS operator that performs side effects (like logging or debugging) without modifying the emitted values in an observable stream.
Use the AuthService on the Login Component
First, we need to inject the dependencies:
- the new
AuthServiceto use theloginmethod - the router to handle the navigation if the login action is successful.
|
|
Then, let’s implement the login method:
|
|
If you wonder about the logout, it’s very simple:
|
|
However, you might have noticed the logout endpoint doesn’t take any parameters. So how do you tell the REST API who’s logging out?
Interceptors
Interceptors allow to modify a HTTP request to add, for example, an HTTP header.
This is what we need to do if we want to call the REST API because it expects the token received on login.
To create a new interceptor, run the Angular command below:
|
|
In the authentication system that Sergio provides, the backend requires an HTTP header Authorization: Token {token value}.
The interceptor acts as a proxy to add some data to HTTP requests, in our case an HTTP header:
|
|
Now, any request to the REST API receives the token in the header and the logout endpoint can retrieve it to log out the session associated.
One last step remains to complete all this work: tell the HTTP client how to use the interceptor we created.
We do so by updating the provideHttpClient to use it:
|
|
We’re almost done! The last thing to code is to prevent users from seeing the pages requiring “authenticated” status.
Guards
Guards will help us with the last part.
Guards run on a selected use case. Those use cases are listed when creating one:
|
|
In our use case, when a route activates, we need to run some code to check if the current user can browse the page.
|
|
To use the guard, we need to update the routes:
|
|
REST API integration
Now that we have implemented the authentication API, implementing a data API won’t be hard.
I’ll just share a best practice about the communication between the Angular application and the API you consume.
Update the Existing Service With API Calls
In our example application, the API returns cards so first, we’ll need to create a interfaces/card.interface.ts to define the contract between the Frontend and the Backend:
|
|
Then, we modify the card.model.ts file so that:
-
it implements the interface.
1export class Card implements ICard {} -
it defines a static method to convert a card in JSON format to a
Cardinstance.1 2 3static fromJson(cardJson: ICard): Card { return Object.assign(new Card(), cardJson); } -
it defines a method to convert a
Cardinstance to a card in JSON format.1 2 3 4 5 6 7toJson(): ICard { const cardJson: ICard = Object.assign({}, this); // The `id` must be removed since it is either necessary (Create) // or it is present in the endpoint URI (Update). delete cardJson.id; return cardJson; }
Then, we can update the CardService to query the REST API:
|
|
Update the List Components
In the card-list components, we have this:
|
|
But ESLint tells us:
|
|
We could use a subscribe on the getAll result to convert the Observable<Card[]> into Card[], but Angular actually provides a simpler method called toSignal:
|
|
And, consequently, you can remove the constructor code.
Update the Single Card Component
In this case, the code adaptation requires a different approach, but again, to avoid nested subscribe, Sergio showcases the use of switchMap in a pipe:
|
|
This way, we only need to unsubscribe one subscription.
On the submit method, we need to adapt the code:
|
|
On the deleteCard, we also need to adapt the code:
|
|
Conclusion
I personally prefer Vue’s syntax. But compared to React, it feels more structured to use Angular.
Regarding the course on YouTube, I think Sergio did a great job and I think I learned all I needed to really understand the basics about Angular.
Also, having a background experience with Vue 3, I understood the signal concepts faster, as I could link the equivalent syntax and API with Vue.
I need to practice now, especially in regards to the pipe, subscribe, tap, map and so on. He teaches RxJs part in another video I’ll review soon.
Deeper Dive Into RxJs and co
At the end of the course, Sergio shares a tip about the best practices when you handle several subscriptions in a single component.
In fact, instead of using one subscription per use case, we can declare only one variable of type Subscription for all:
|
|
Then, in each use case, we perform the following:
|
|
And we update the ngOnDestroy body to this:
|
|
For more on the topic, I recommend Sergio’s vlogs on the topic.
- Intro à RxJS - Observables, Observers, Subscriptions
- RxJS / Angular : Opérateurs et exemples concrets
Follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Photo by RealToughCandy.com.