Standalone Angular Tour Of Heroes

The popular Angular Tutorial Tour of Heroes currently does not work as described with recent Angular versions (Angular 17) 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

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 17).

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 17.

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.

Visual Studio Code problem pop-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.

Visual Studio Code quick fix

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
This website uses cookies and similar technologies to provide certain features, enhance the user experience and deliver content that is relevant to your interests. Depending on their purpose, analysis and marketing cookies may be used in addition to technically necessary cookies. By clicking on "Agree and continue", you declare your consent to the use of the aforementioned cookies. Here you can make detailed settings or revoke your consent (in part if necessary) with effect for the future. For further information, please refer to our Privacy Policy.