3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 2

Structure of an Angular application

Understanding how an Angular app works

STANDARD

Structure of an Angular application

The goal of this lesson is to make Angular seem a little less magical, and to gain a basic understanding of how Angular does what it does. What Angular actually does goes very deep, but I think we can gain a basic surface-level understanding of the key ideas. Ideally, we want to get to the stage where we know why certain files exist, and why we are using certain methods, rather than treating things as just some magical thing you need to do to make Angular work.

This lesson will focus primarily on the basic files within an Angular application, and we will dig even further into other key concepts throughout the rest of this module.

Create a new Angular application

So that we have something to reference, we are going to create a new standard Angular application using the Angular CLI. If you do not already have the Angular CLI installed you can do so with the following command:

npm install -g @angular/cli

You can then create a new project with:

ng new

You can use the following configuration:

? What name would you like to use for the new workspace and initial project? my-app
? Would you like to add Angular routing? (y/N) y
? Which stylesheet format would you like to use? SCSS

You can then open the project in whatever code editor you like, e.g. Visual Studio Code:

code my-app

I highly encourage you throughout this entire module to reference this application, add code snippets from the lessons, come up with your own code snippets, and just see what happens. Don’t be afraid of breaking the application — we won’t be using this application for anything later, and you can always just delete it and generate a new one anyway.

Start at the beginning

Angular has some special things going on, but in the end… it is basically just a website. The first thing that loads when we access a website is the index.html file. Let’s see what the index.html file for Angular looks like:

src/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MyApp</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

A pretty standard looking HTML document. The only curious part is this:

<body>
  <app-root></app-root>
</body>

This is where all of the magic for Angular kicks off. Inside of the <body> tag of this document we are adding our root component.

The Root Component

This root component is an Angular component. We always have this component by default. We can find it here:

src/app/app.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'my-app';
}

We are going to talk more about the @Component decorator soon, but notice the selector. That is the tag name that we used in our index.html file. The selector is how Angular knows to compile this component and inject it into the DOM wherever <app-root></app-root> is.

You might also notice that this component links out to a separate file for its template:

src/app/app.component.html:

<router-outlet></router-outlet>

IMPORTANT: The default template for a newly generated Angular application contains a lot of placeholder content. You should replace the entire template with just the code above.

NOTE: To use the <router-outlet> template in the template, we must have RouterOutlet in the imports array in app.component.ts (this is already added for us by default)

The main role of our root component (AppComponent) is to serve as a container for our application. Generally speaking, almost all Angular applications have a single root component, and within that root component will be more components, which might also have more components, making up a tree of nested components:

Angular Application Tree

NOTE: We will typically use the <router-outlet> in our applications to switch between components. In which case, the diagram above would just have the AppComponent and the <router-outlet> as a child node. But, to give a better sense of the relationship between components, the diagram above does not use <router-outlet> and assumes we are not using routing. This way, we can represent our application as one big tree which is a bit easier to think about for now.

To create the structure in the diagram above, our template might look like this (assuming that we had already created all these additional components):

<app-layout>
  <app-header></app-header>
  <app-content>
    <app-button></app-button>
  </app-content>
</app-layout>

instead of like this:

<router-outlet></router-outlet>

We are going to stick with the <router-outlet> example for the rest of this lesson.

If you serve the application we created with:

ng serve

You will notice that we just have an empty page. That is because we have only added the <router-outlet> which has the responsibility of switching between different components based on the current route in the URL bar. However, we don’t have any components to display yet, so we just have a blank page.

We don’t have to have other components. For example, we could just define a template for the root component directly (go ahead and try this):

<h2>This is my entire app!</h2>

and we would see this rendered out in the browser:

No router outlet

But we generally don’t want to define our entire application inside of one component. This would not allow for routing between different components, and one of the powerful things about Angular is that we can easily break up an application into different components which have their own responsibilities (rather than having one big jumbled mess).

Make sure to change the template back to the router outlet:

<router-outlet></router-outlet>

Bootstrapping

We understand now that the root component is added to our index.html file and serves as a basic container for the rest of the application. We can add additional components inside of the root component, or we can navigate to those additional components by utilising the <router-outlet> inside of the root component.

But we haven’t really dug deep enough here. We are defining a root component and using it in index.html but there are more steps involved in how this actually happens. An important file in kicking off an Angular application is:

src/main.ts

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));

We need to “bootstrap” our root component before we can use it — in the code above we supply our AppComponent as the root component, along with an appConfig from another file. Go find that config file and take a quick look — you will find for now that it is just configured the routes for the application, but more types of configurations can be added as well.

This brings us to the first of those situations I mentioned at the beginning of this course, where there is both a “new” way and an “old” way to do things. We are looking at the “new” way to bootstrap an application using standalone components.

With standalone components we treat the component themselves as the basic building blocks of the application, and our root component is simply a component.

However, you might come across some applications that are not bootstrapped with the AppComponent but rather an NgModule that declares that component.

Before standalone components, a component could only exist within a module which provides its compilation context. An @NgModule is like an isolated little world for the component, it will only know about things that are included in the module it belongs to. A component without an @NgModule would have been like a person without a universe to contain it.

Perhaps even without the historical context you can see how the new approach with standalone components is a simpler mental model.

I’m trying to keep this introduction to Angular high-level and simple for now. We will talk more about NgModule later, but for now, just keep in mind that you might come across Angular applications that are bootstrapped like this instead:

platformBrowserDynamic().bootstrapModule(AppModule) .catch(err =>
console.error(err));

Either way, the basic idea of bootstrapping is still essentially the same. To kick off our Angular application, we either bootstrap the root component or the root module which will contain the root component.

This is how that <app-root> tag in our index.html file renders our application.

The approach with standalone is clear enough for now — we just supply the component we want to use as the root component and that is it. But let’s also take a quick look at what it might look like using an @NgModule (even though we won’t be doing this ourselves).

The @NgModule would look something like this:

src/app/app.module.ts

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Notice that this module has our root component as a declaration. That means that our root component is available within this module. If we were to declare two different components in this module, then those components would know about each other’s existence. If we tried to access those components from a component in another module, then it wouldn’t know that these components exist. It would be in its own little separate container world.

We also have some imports. This allows us to import functionality from other modules and make them available in this module — this would allow our AppComponent to access functionality from the AppRoutingModule for example.

We can also actually supply imports directly to our standalone components. You can see that we are doing this already:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'my-app';
}

With the NgModule concept we declare things within this shared container/world idea, which is how we share functionality between different things. Standalone components still need to do this same thing — in the code above our standalone component wants to import functionality from the RouterOutlet, and it does this by supplying an imports array.

Standalone and NgModules are actually compatible with each other, so you can use a mixture of the two if you like. You can, for example, import an NgModule into a standalone component (or you might just import other standalone components into your standalone component).

Don’t worry if this is feeling like a whole lot of concepts all at once, not all of this information is going to stick right away. Our main goal with these introductory theory lessons is just to get some idea of the basic landscape.

Although we might not know exactly what is happening here at the level of the Angular compiler, we now have a complete picture of how our root component is able to be supplied to the index.html page.

  1. Our root component or root module is bootstrapped in main.ts
  2. Bootstrapping the component creates it and adds it to the DOM, making it available for use in index.html
  3. With our root component bootstrapped, we can then add other components within it to create our application

Creating a new component

At this point, we already have a pretty good picture of how an Angular application “works”. In the following lessons, we will focus on building out our knowledge with additional concepts that we can use to actually create a full application.

However, at this point, we still just have a blank page, so I want to take this walkthrough just a little further. As I mentioned, our root component is really just a container for the rest of our application. What we are going to do now is create a simple new component and add it to our root component so that we actually have something to look at on the screen.

First, we are going to create the component and nest it directly within the root component. Then, instead of just adding it inside of our root component, we are going to route to that component instead.

NOTE: Whenever you see me referencing a folder path that does not exist, you should create those folders yourself. For example, the following instruction will require that you create your own home folder inside of the app folder.

Create a new file at src/app/home/home.component.ts and add the following:

import { Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-home',
  template: ` <p>I am the home component</p> `,
})
export class HomeComponent {}

We have our component defined, now we want to display it in the application. One way we can do this is just to add it to the root component.

Try adding the HomeComponent to the root AppComponent template

<app-home></app-home>
<router-outlet></router-outlet>

NOTE: If you have not already deleted all of the default boilerplate from the app.component.html file make sure to do that now and only include just the code above in the template.

We are just trying to display the component alongside the router outlet for now, more on that later. Although we have used the correct selector, you will notice that this does not work. Our code editor will likely complain about it, and the application will fail to compile with this error:

✘ [ERROR] NG8001: 'app-home' is not a known element:
1. If 'app-home' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-home' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. [plugin angular-compiler]

Take a second to think why this might be happening — don’t worry if you’re not sure why.

We are trying to use the HomeComponent inside of our AppComponent… but the AppComponent does not know the HomeComponent exists!

If we are using standalone components, then we need to import the component we want to use directly into the component where we want to use it.

Make sure to import the HomeComponent into the AppComponent

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { HomeComponent } from "./home/home.component";

@Component({
    selector: 'app-root',
    standalone: true,
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    imports: [CommonModule, RouterOutlet, HomeComponent]
})
export class AppComponent {
  title = 'my-app';
}

IMPORTANT: There are actually two imports here. One at the top to import HomeComponent into the file, but we also need to add it to the imports array inside of our @Component — we need to do this so that the Angular compiler knows about it.

NOTE: Although you can just type these imports out manually, if you are using the Angular Language Service extension it can do it automatically for you through executing a code action (try right-clicking the error in the template or hitting the keyboard shortcut in your editor for triggering code action)

Now our application will compile, and we will be able to see our new component in the browser!

Home component

Routing to the component

Let’s highlight a problem, and the point of the <router-outlet> by introducing another new component. The intent of our first component was to have it serve as our Home page. Let’s create a page now that will serve as a Settings page.

Create a new file at src/app/settings/settings.component.ts and add the following:

import { Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-settings',
  template: ` <p>I am the settings component</p> `,
})
export class SettingsComponent {}

Let’s do the same thing that we did for our home component — we will add it to the imports for our root component, and add it to the template of the root component. See if you can do this yourself before looking at the solution.

Import the SettingsComponent into AppCommponent:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { SettingsComponent } from './settings/settings.component';

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  imports: [CommonModule, RouterOutlet, HomeComponent, SettingsComponent],
})
export class AppComponent {
  title = 'my-app';
}

Add the SettingsComponent to the root components template:

<app-home></app-home>
<app-settings></app-settings>
<router-outlet></router-outlet>

We should now see something like this in the browser:

Settings component

Not exactly what we want… we don’t want to display both the home page and the settings page at the same time. This is where the <router-outlet> comes into the picture. Instead of adding these components directly to the root component, we want to route to them.

Remove the two custom components from the root component template:

<router-outlet></router-outlet>

To configure which components should display, and when, we will need to define some routes. We can do this in our src/app/app.routes.ts file. We are going to discuss routing in more detail in a later lesson. The key idea to understand for now is that we can pass some routes into to the Angular router and that will control what the <router-outlet> displays.

Modify src/app/app.routes.ts to reflect the following:

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'home',
    loadComponent: () =>
      import('./home/home.component').then((m) => m.HomeComponent),
  },
  {
    path: 'settings',
    loadComponent: () =>
      import('./settings/settings.component').then((m) => m.SettingsComponent),
  },
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full',
  },
];

This array defines which component we want to activate for a particular route. We also have a default path that will redirect to the home route.

We don’t actually need to make any changes here, but for completeness, you might want to take a look at the app.config.ts file:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};

This code is set up by default, but this is where the routes we are defining are actually passed to the Angular router.

Now if we go to http://localhost:4200/ (because of our default path) or http://localhost:4200/home we will see the HomeComponent and if we go to http://localhost:4200/settings we will see the SettingsComponent.

As always, there is much more to learn about routing. For now, this is enough for us to get a picture of how <router-outlet> works. It’s important to understand the difference between a component that is displayed within another component by adding it to its template and a routed component.

Going deeper

Now we have two “page” or “routed” components (later we will also refer to these as “smart” components): the home page and the settings page. We change which one of these is displayed based on the current active route.

But we don’t just have routed components. We can have additional components within our page/view components to help keep our application more modular. We could just define the entire template for our home page within the HomeComponent itself, or we could create more components to nest within it.

We are going to create one more component to finish off this lesson. Let’s say we want a WelcomeComponent that just displays the message Hi, Josh! on the home page.

Create a component at src/app/home/ui/welcome.component.ts and add the following:

import { Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-welcome',
  template: ` <p>Hi, Josh!</p> `,
})
export class WelcomeComponent {}

NOTE: The location of this component (i.e. home/ui) is a convention we will be using in this course, but it does not have to be here. We will talk more about this folder structure later and its benefits, but this component could exist anywhere in your project.

Modify the HomeComponent to reflect the following:

import { Component } from '@angular/core';
import { WelcomeComponent } from './ui/welcome.component';

@Component({
  standalone: true,
  selector: 'app-home',
  template: `
    <app-welcome />
    <p>I am the home component</p>
  `,
  imports: [WelcomeComponent],
})
export class HomeComponent {}

NOTE: Notice that we are defining the entire component, including the template, in a single file (rather than having a file specifically for the template). You can use either style, but this is the style we will be using in this course.

Now if we look at our home page:

Welcome component

We can see that our WelcomeComponent is being displayed inside of our HomeComponent. This can go as many layers deep as you like — you might also decide to add another component inside of WelcomeComponent.

Our full component tree now looks something like this (again, this is somewhat simplified):

Component tree

The reason we see what we see on screen is because:

  1. Our root component has a <router-outlet> in its template
  2. We have a route set up to display the HomeComponent in the <router-outlet> when the path is /home (or /)
  3. We have added our WelcomeComponent to the template of our HomeComponent

Recap

We’ve covered quite a lot this lesson. The rest of this module is made up of lessons that cover individual concepts in a lot of detail, but the goal of this lesson was to paint the big picture whilst avoiding diving too deep into individual concepts (although that was required to some degree).

I’ve thrown a lot at you, so don’t worry if it hasn’t all stuck right away — we will continue addressing these concepts and more.

What best describes the typical role of the root component?

What is required in order to use the app-root tag in our index.html file?

The only way to display a component is to configure a route for it