Standalone Angular Tour Of Heroes
The popular Angular Tutorial Tour of Heroes currently does not work as described with recent Angular versions (Angular 18) because the Angular command-line tool now creates a standalone Angular app by default. You could avoid these problems by creating a non-standalone app but it is also quite instructive to just fix the problems yourself and stick with the standalone version.
Table Of Contents
HeroesComponent
uppercase
Pipe
HeroesComponent
Template
HeroComponent
MessageService
AppRoutingModule
AppRoutingModule
...
routerLink
HttpClient
About the "Tour of Heroes"
The "Tour of Heroes" was the standard beginner's tutorial for Angular up to version 17. It has now been replaced with a new tutorial, see https://angular.dev/tutorials/learn-angular. The following refers to the version 17 documentation of Angular but the accompanying git repository has been updated to Angular 18. You should therefore be able to follow with Angular 18, as well.
What Is Standalone Mode?
In the past, Angular applications were structured into modules that served as the infrastructure for wiring the different components and modules together. However, Angular modules had a considerable overhead. Starting with Angular 14, you can now avoid this overhead by using standalone components, which are more light-weight and also more flexible.
This leads to a revaluation of the bootstrapping code in the application entry file src/main.ts
which takes over dependency injection tasks from Angular modules.
You can read more about this in Getting started with standalone components in the Angular docs.
Unfortunately, the popular Angular Tour of Heroes tutorial has not been updated to reflect these changes. This blog post jumps in and shows how to fix the errors that occur, if you create the tutorial app with standalone components which is the default at the time of this writing (Angular 18).
You can have a look at the final app in the accompanying Git repository standalone-angular-tour-of-heroes. The commits in the repo should roughly follow the tutorial steps.
Differences to the Tutorial Steps
This blog post is not a re-write of the tutorial but just highlights the measures that you have to take to make the code work with Angular 18.
Create a Project
In general, it is a good idea to generate Angular apps in strict mode as this avoids some errors in the TypeScript code.
$ npx ng new --strict standalone-angular-tour-of-heroes
Select CSS as the stylesheet format and "No" for server-side-rendering as this is not needed. This will now create an Angular application without the file src/app/app.module.ts
. Therefore, whenever the tutorial mentions that file, you know that there is something that you have to change.
For now, just follow the original Tour of Heroes tutorial until you run into the first error.
The Hero Editor
Generating the HeroesComponent
Generating the HeroesComponent
works but, when you insert it into the template src/app/app.component.html
you will face the first problem:
✘ [ERROR] NG8001: 'app-heroes' is not a known element:
1. If 'app-heroes' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-heroes' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. [plugin angular-compiler]
src/app/app.component.html:2:0:
2 │ <app-heroes></app-heroes>
╵ ~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
src/app/app.component.ts:8:14:
8 │ templateUrl: './app.component.html',
╵ ~~~~~~~~~~~~~~~~~~~~~~
In the past, ng generate component
automatically updated the module that wired the components inside of that module together. Now, each component is individually usable and you have the responsibility of importing it into other components that need it. Do so by making the the following change to src/app/app.component.ts
:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, HeroesComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
title = 'Standalone Tour of Heroes';
}
It is crucial, that you also set Component.standalone
to true
. Otherwise, you will get an error:
[ERROR] TS-992010: 'imports' is only valid on a component that is standalone. [plugin angular-compiler]
It tells you that you can only import modules and components into standalone components.
The above listing shows the complete source file. From now on, I will just show the changes to the actual code, and you should add the import
statements at the head of the file as needed. If you use an IDE for coding, you can probably do that semi-automatically.
For example, after adding the HeroesComponent
to the imports
array, the popular IDE Visual Studio Code will highlight it with tildes (~~~~~
) because you did not import the symbol from src/app/heroes/heroes.component.ts
. If you move your mouse over the error location, a more detailed problem description will show up.
So the problem is that HeroesComponent
cannot be found. If you click on Quick fix
, another popup is displayed, this time with suggestions on how to fix the problem.
The suggestion to add an import from ./heroes/heroes.component
is correct. Select it, and Visual Studio Code will add the missing import
statement at the top of the file.
The application should now compile and work as expected.
Format with the uppercase
Pipe
When you add the uppercase
pipe to the HeroesComponent
template, you will get the next error:
✘ [ERROR] NG8004: No pipe found with name 'uppercase'. [plugin angular-compiler]
src/app/heroes/heroes.component.html:1:19:
1 │ <h2>{{ hero.name | uppercase }} Details</h2>
╵ ~~~~~~~~~
Error occurs in the template of component HeroesComponent.
src/app/heroes/heroes.component.ts:8:14:
8 │ templateUrl: './heroes.component.html',
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~
The AppModule
normally imports the CommonModule
from @angular/common
. You now have to do that yourself in src/app/app.component.ts
. Change the invocation of the @Component
decorator function as follows:
@Component({
selector: 'app-heroes',
standalone: true,
imports: [CommonModule],
templateUrl: './heroes.component.html',
styleUrl: './heroes.component.css',
})
Make sure that @Component.standalone
is true
and do not forget to import
CommonModule
at the top of the file!
Edit the Hero
There is another error:
✘ [ERROR] NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. [plugin angular-compiler]
Now, the FormsModule
is missing, and it cannot be imported by the AppModule
because there is no AppModule
. It must be imported inside src/app/heroes/heroes.component.ts
, just like the CommonModule
before:
@Component({
selector: 'app-heroes',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './heroes.component.html',
styleUrl: './heroes.component.css',
})
Make sure that @Component.standalone
is true!
Create a Feature Component/Update the HeroesComponent
Template
Updating the HeroesComponent
template to display the HeroDetailComponent
triggers the next error:
[ERROR] NG8001: 'app-hero-detail' is not a known element:
...
The problem should be clear by now. The HeroDetailComponent
has to be imported by the HeroesComponent
:
@Component({
selector: 'app-heroes',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './heroes.component.html',
styleUrl: './heroes.component.css',
})
But there are more warnings and errors:
...
▲ [WARNING] NG8103: The `*ngIf` directive was used in the template, but neither the `NgIf` directive nor the `CommonModule` was imported. Use Angular's built-in control flow @if or make sure that either the `NgIf` directive or the `CommonModule` is included in the `@Component.imports` array of this component. [plugin angular-compiler]
...
✘ [ERROR] NG8004: No pipe found with name 'uppercase'. [plugin angular-compiler]
...
✘ [ERROR] NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. [plugin angular-compiler]
...
Well, we are using the ngIf
directive without having it imported. We already had the missing pipe uppercase
before, and ngModel
is also not known. This is fixed in src/app/hero-detail.component.ts
by importing a couple of symbols:
@Component({
selector: 'app-hero-detail',
standalone: true,
imports: [CommonModule, FormsModule, NgIf],
templateUrl: './hero-detail.component.html',
styleUrl: './hero-detail.component.css',
})
Add Services
Update HeroComponent
When declaring the property heroes
of the HeroComponent
, the TypeScript compiler complains once again:
✘ [ERROR] TS2564: Property 'heroes' has no initializer and is not definitely assigned in the constructor. [plugin angular-compiler]
This happens because we have enabled strict mode when generating the application. More precisely, it is caused by our settings in the top-level tsconfig.json
. We can shut it up by adding a question mark in src/app/heroes/heroes.component.ts
:
export class HeroesComponent {
heroes?: Hero[];
selectedHero?: Hero;
// ...
}
Now, heroes
is marked as optional, just as selectedHero
before, and everything works again.
Create Message Component
When you add the message component to the application template src/app/app.component.html
you will get another error:
✘ [ERROR] NG8001: 'app-messages' is not a known element:
Well, we have been through that before and now what we have to do. We have to update the imports of AppComponent
in src/app/app.component.ts
:
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, HeroesComponent, MessagesComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
Bind to the MessageService
The MessagesComponent
uses the *NgIf
and *NgFor
directives. That will trigger warnings:
▲ [WARNING] NG8103: The `*ngIf` directive was used in the template, but neither the `NgIf` directive nor the `CommonModule` was imported. Use Angular's built-in control flow @if or make sure that either the `NgIf` directive or the `CommonModule` is included in the `@Component.imports` array of this component. [plugin angular-compiler]
...
▲ [WARNING] NG8103: The `*ngFor` directive was used in the template, but neither the `NgFor` directive nor the `CommonModule` was imported. Use Angular's built-in control flow @for or make sure that either the `NgFor` directive or the `CommonModule` is included in the `@Component.imports` array of this component. [plugin angular-compiler]
In order to shut these warnings up, you have to import the directives in src/app/messages/messages.component.ts
:
@Component({
selector: 'app-messages',
standalone: true,
imports: [NgFor, NgIf],
templateUrl: './messages.component.html',
styleUrl: './messages.component.css',
})
You have to import
them at the top of the file from @angular/common
.
Add Navigation with Routing
One of the big advantages of standalone mode is the way that routing is implemented. This has actually become a lot easier and more straightforward.
Add the AppRoutingModule
Or rather, do not add this module. It is no longer needed. So, skip that step from the tutorial and follow the instructions given here.
First have a look at the application main entry point src/main.ts
. It should look like this:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));
As you can see, the function bootstrapApplication()
is called with two arguments. The first one is a component, and the second one an optional argument of type ApplicationConfig
which is essentially an object.
The configuration resides in its own source file src/app/app.config.ts
:
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
For non-standalone mode, modules provide other modules like RouterModule
. Since the module layer has been removed, this task is performed by the bootstrapping code of the application, and Angular has added canned providers the names of which usually follow the naming convention provide*SOMETHING*
, in our case provideRouter
.
Compare that code with the AppRoutingModule
from the original tutorial:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
const routes: Routes = [
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
This module imports the RouterModule
for the application root with the routes as an argument. This is simply replaced by a call to provideRouter
, again with a Routes
array as its argument.
To keep things well-structured and clean, the routes
are imported from a separate file src/app/app.routes.ts
:
import { Routes } from '@angular/router';
export const routes: Routes = [];
And this is exactly the location where you should define routes. All you have to do for defining the first route is to add it to src/app/app.routes.ts
like this:
import { Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
export const routes: Routes = [
{ path: 'heroes', component: HeroesComponent },
];
You can now add the <router-outlet></router-outlet>
to the application component template as described in the tutorial. If you point your browser to http://localhost:3000/heroes it should now follow the route and display the heroes component.
But I Did Add the AppRoutingModule
...
What if you had just followed the instructions from the tutorial. What errors would you face?
If you followed the instructions above, you can simply jump to the next section Add a Navigation Link Using routerLink
. This section is mostly here so that people that google the error messages will find a solution.
Generating the AppRoutingModule
as described in the tutorial, would already fail:
$ ng generate module app-routing --flat --module=app
Specified module 'app' does not exist.
Looked in the following directories:
/src/app/app-routing
/src/app/app
/src/app
/src
From the error message you would probably understand that you have to omit the option --module=app
since we are using standalone components, not modules. Without the option, it would succeed:
$ ng generate module app-routing --flat
CREATE src/app/app-routing.module.ts (196 bytes)
Now, when you modify src/app/app.component.html
, and replace <app-heroes></app-heroes>
with <router-outlet></router-outlet>
it seems to work at first. You open http://localhost:4200
, and you just see the title of the application, just as described in the tutorial. But what if you enter http://localhost:4200/heroes
in the address bar? Nothing seems to happen.
If you open the browser's JavaScript console, it reveals the error:
main.ts:5 ERROR Error: NG04002: Cannot match any routes. URL Segment: 'heroes'
at Recognizer.noMatchError (router.mjs:3687:12)
at router.mjs:3720:20
at catchError.js:10:39
at OperatorSubscriber2._this._error (OperatorSubscriber.js:25:21)
at Subscriber2.error (Subscriber.js:43:18)
at Subscriber2._error (Subscriber.js:67:30)
at Subscriber2.error (Subscriber.js:43:18)
at Subscriber2._error (Subscriber.js:67:30)
at Subscriber2.error (Subscriber.js:43:18)
at Subscriber2._error (Subscriber.js:67:30)
Why does this happen? Theoretically, the AppRoutingModule
should work as expected. But the problem is that you do not use it anywhere in the application. Check the TypeScript files! None of them has an import
from src/app/app-routing.module.ts
. That means, that there are no routes at all defined, and that is why no routes can be matched.
So, why not fixing it by importing the AppRoutingModule
from the AppComponent
in src/app/app.component.ts
?
@Component({
selector: 'app-root',
standalone: true,
// Adding the AppRoutingModule does NOT work!
imports: [RouterOutlet, HeroesComponent, MessagesComponent, AppRoutingModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
Then, the JavaScript console will throw new errors:
main.ts:5 ERROR Error: NG04007: The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead.
at Object.provideForRootGuard [as useFactory] (router.mjs:7445:11)
at Object.factory (core.mjs:3322:38)
at core.mjs:3219:47
at runInInjectorProfilerContext (core.mjs:866:9)
at R3Injector.hydrate (core.mjs:3218:21)
at R3Injector.get (core.mjs:3082:33)
at injectInjectorOnly (core.mjs:1100:40)
at ɵɵinject (core.mjs:1106:42)
at Object.RouterModule_Factory [as useFactory] (router.mjs:7376:41)
at Object.factory (core.mjs:3322:38)
Okay, the error message suggests to import with RouterModule.forChild()
instead. Try it out in src/app/app-routing.module.ts
:
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
That fixes the problem for the start page http://localhost:4200
. But when you go to http://localhost:4200/heroes
, you see the same error message as before, "ERROR Error: NG04002: Cannot match any routes. URL Segment: 'heroes'".
So what can you do? Delete src/app/app-routing.module.ts
and follow the instructions above in the section Add Navigation with Routing. It is actually a lot easier.
Add a Navigation Link Using routerLink
You have added the link with routerLink
to the ApplicationComponent
template but you can actually not click the link. And Angular is being nasty and does not even produce an error in the JavaScript console.
That problem is owed to the fact that the template snippet <a routerLink="/heroes">
is perfectly legal HTML, just with an unknown attribute routerLink
that is ignored by the browser.
In order to give it a special meaning, you first have to import the RouterModule
in src/app/app.component.ts
:
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterModule, HeroesComponent, MessagesComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
You have to import
the symbol RouterModule
from @angular/router
for making that work.
Now you can click the link, and it will bring you to the list of heroes.
Add a Dashboard
The tutorial tells you to add the routes to src/app/app-routing.module.ts
. But we have no module. Instead, you add them to src/app/app.routes.ts
. In the end, the routes
array should look like this:
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'heroes', component: HeroesComponent },
{ path: 'dashboard', component: DashboardComponent },
];
But things will not work as expected. In the JavaScript console you will see an error:
dashboard.component.html:3 NG0303: Can't bind to 'ngForOf' since it isn't a known property of 'a' (used in the '_DashboardComponent' component template).
1. If 'a' is an Angular component and it has the 'ngForOf' input, then verify that it is a part of an @NgModule where this component is declared.
2. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.
We have to import NgFor
from @angular/common
in the dashboard component src/app/dashboard/dashboard.component.ts
:
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [NgFor],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css'],
})
Make sure that @Component.standalone
is true!
Navigate to Hero Details
We have no AppRoutingModule
. We therefore have to add the new route to src/app/app.routes.ts
:
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'heroes', component: HeroesComponent },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
];
When you add the routerLink
to src/app/dashboard/dashboard.html
, you run into an error that you already know:
✘ [ERROR] NG8002: Can't bind to 'routerLink' since it isn't a known property of 'a'. [plugin angular-compiler]
That means that you also have to import the RouterModule
from src/app/dashboard/dashboard.ts
:
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [NgFor, RouterModule],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css'],
})
You will get the same error for the HeroesComponent
. Add the missing import to src/app/heroes/heroes.component.ts
:
@Component({
selector: 'app-heroes',
standalone: true,
imports: [CommonModule, NgFor, RouterModule],
templateUrl: './heroes.component.html',
styleUrl: './heroes.component.css',
})
Find the Way Back
When you implement the goBack()
method in the HeroDetailComponent
you may run into this problem:
✘ [ERROR] TS2339: Property 'back' does not exist on type 'Location'. [plugin angular-compiler]
In this case check whether you have this import
statement:
import { CommonModule, Location, NgIf } from '@angular/common';
It is crucial that you import
Location
from @angular/common
because there is a universally available interface
Location
of the same name. This is exactly what you are using, when you do redirects in JavaScript with document.location.href = 'somewhere/else'
. But what we need is the Location
class from @angular/common
.
Get Data From a Server
Enable HttpClient
The tutorial tells you to import HttpClientModule
into the AppModule
. But since there is no AppModule
in our standalone application, we have to import it into the components that use it. So just ignore that import for now.
Also ignore the other modifications to AppModule
that you are asked to do.
But after you have changed the other components as described to the tutorial, it is time to fix the import of the HttpClientModule
because you get an error in the JavaScript console:
main.ts:5 ERROR NullInjectorError: R3InjectorError(Standalone[_DashboardComponent])[_HeroService -> _HeroService -> _HeroService -> _HttpClient -> _HttpClient]:
NullInjectorError: No provider for _HttpClient!
at NullInjector.get (core.mjs:1654:27)
at R3Injector.get (core.mjs:3093:33)
at R3Injector.get (core.mjs:3093:33)
at injectInjectorOnly (core.mjs:1100:40)
at Module.ɵɵinject (core.mjs:1106:42)
at Object.HeroService_Factory [as factory] (hero.service.ts:11:25)
at core.mjs:3219:47
at runInInjectorProfilerContext (core.mjs:866:9)
at R3Injector.hydrate (core.mjs:3218:21)
at R3Injector.get (core.mjs:3082:33)
You are trying to inject the HttpClient
from the HttpClientModule
into the constructor of HeroService
but there is no provider for the HttpClientModule
. The error message also gives you a hint where to fix this problem, namely in src/main.ts
, in the call to bootstrapApplication()
.
Since the application configuration is moved from src/main.ts
(check it!), you have to edit src/app/app.config.ts
instead, where you can configure the provider, just like you did with the RouterModule
before.
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideHttpClient()],
};
You have to import
provideHttpClient()
from @angular/common/http
.
But the application is still not working. The JavaScript console shows another error:
ERROR HttpErrorResponse {headers: _HttpHeaders, status: 200, statusText: 'OK', url: 'http://localhost:4200/api/heroes', ok: false, …}
The error message is not very helpful but the problem is that there is no provider for the HttpClientInMemoryWebApiModule
that is used here. The procedure is a little bit different from that for the RouterModule
and HttpClientModule
because you have to pass an argument to the forRoot()
method of the HttpClientInMemoryWebApiModule
. Try this:
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
importProvidersFrom([
HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {
dataEncapsulation: false,
}),
]),
],
};
This is the final version of src/app/app.config.ts
.
Add a Hero
Whilst implementing the method add()
in src/app/heroes/heroes.component.ts
, you will get this error:
✘ [ERROR] TS2532: Object is possibly 'undefined'. [plugin angular-compiler]
Fix this by adding a question mark to heroes
, marking it as optional.
add(name: string): void {
name = name.trim();
if (!name) {
return;
}
this.heroService.addHero({ name } as Hero).subscribe(hero => {
this.heroes?.push(hero);
});
}
Implementing the Search
You will once more run into an error, when adding the search to the dashboard.
✘ [ERROR] NG8001: 'app-hero-search' is not a known element:
The solution is clear now. Update src/app/dashboard/dashboard.component.ts
.
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [NgFor, RouterModule, HeroSearchComponent],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css'],
})
When you add the routerLink
to the HeroSearchComponent
template, you will get this well-known error:
✘ [ERROR] NG8002: Can't bind to 'routerLink' since it isn't a known property of 'a'. [plugin angular-compiler]
This one is new:
✘ [ERROR] NG8004: No pipe found with name 'async'. [plugin angular-compiler]
Fix both in src/app/hero-search/hero-search.component.ts
:
@Component({
selector: 'app-hero-search',
standalone: true,
imports: [CommonModule, RouterModule, NgFor],
templateUrl: './hero-search.component.html',
styleUrl: './hero-search.component.css',
})
While you are at it, you also import NgFor
to fix the warning that we already know.
Try the application out! Everything should work by now. Congrats!
Leave a comment