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';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
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.
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:
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. 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:
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 { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
We need to bootstrap our root component before we can use it… or more specifically, we need to bootstrap the Angular module that declares this component.
I’m trying to keep this introduction to Angular high-level and simple for now. However, we’re skating on thin ice here for a variety of reasons:
- It is easier to think about components as being the “building blocks” of an Angular application, but at the moment the actual building blocks are the modules that contain those components
- The new standalone components approach in Angular fixes this - it allows us to get rid of modules and have components just exist in their own right and be the basic building blocks of an Angular application. This is much more beginner-friendly
- However, standalone components are somewhat new and experimental and most applications still use Angular modules (and likely will for some time) so we can’t just do away with modules and not learn them right now
- I don’t want to get into the nuts and bolts of how modules work in this lesson, we are going to tackle that in its own lesson
So, in the spirit of not diving too deep right away, my explanation of what is happening here might be a bit shallow. We will address this in much more detail later, including this whole standalone components vs Angular modules debacle.
Ok, back to our example. This is the key bit of code we want to focus on:
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
To kick off our Angular application, we bootstrap the root module which will contain the root component. Forgetting about standalone components for the moment, a component can 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.
Let’s take a look at our root AppModule
:
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. The bootstrap
property here you will only see in your root module - this is where we supply the component that is going to be created and inserted into the index.html
file.
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.
- Our root module
AppModule
is supplied tobootstrapModule
inmain.ts
- The root module declares our root component
AppComponent
and bootstraps it - Bootstrapping the component creates it and adds it to the DOM, making it available for use in
index.html
- 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({
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 rootAppComponent
template
<app-home></app-home>
<router-outlet></router-outlet>
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:
1. If 'app-home' is an Angular component, then verify that it is part of this module.
2. If 'app-home' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
Take a second to think why this might be happening - don’t worry if you’re not sure why.
Click here to reveal solution
Solution
We are trying to use the HomeComponent
inside of our AppComponent
, and the AppComponent
is declared in the root AppModule
:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Remember, this module makes up the compilation context for everything inside of it, it’s like its own little world. Nowhere in this world was our new HomeComponent
brought into it. Therefore, our AppComponent
has no idea that it exists.
We can fix this by declaring HomeComponent
inside of the module as well.
Declare
HomeComponent
inside ofAppModule
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
@NgModule({
declarations: [AppComponent, HomeComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Now our application will compile, and we will be able to see our new component in the browser!
This is a simplistic approach that we generally won’t use. We will discuss modules and approaches to using them in much more detail in a later lesson, for now, let’s just stay focused on the basics.
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({
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 declare it in the root module and add it to the root components template. See if you can do this yourself before looking at the solution.
Click here to reveal solution
Solution
Add the
SettingsComponent
to the rootAppModule
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { SettingsComponent } from './settings/settings.component';
@NgModule({
declarations: [AppComponent, HomeComponent, SettingsComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
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:
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:
<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-routing.module.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 the RouterModule
and that will control what the <router-outlet>
displays.
Modify
src/app/app-routing.module.ts
to reflect the following:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { SettingsComponent } from './settings/settings.component';
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'settings',
component: SettingsComponent,
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Now if we go to http://localhost:4200/
or http://localhost:4200/home
we will see the HomeComponent
and if we go to http://localhost:4200/settings
we will see the SettingsComponent
.
Again, this is a simplistic approach to routing and one that we won’t generally be using in the course. Instead of loading our components directly, we will load modules that contain those components, which allows us to lazy load the routes (i.e. we don’t need to immediately load all of the code for all of our routes). We will focus on that in the lesson dedicated to routing (as well as demonstrating how standalone components compares to the module approach).
For now, though, 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: 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({
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.
Declare this component in the root module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { WelcomeComponent } from './home/ui/welcome.component';
import { SettingsComponent } from './settings/settings.component';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
SettingsComponent,
WelcomeComponent,
],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
NOTE: Maybe you can sense a problem here. We have created this component specifically for our home page, yet we are declaring it in the root module. If we did this for all of our components, our root module would become very bloated. This is just because we are taking a simple approach for now. As I mentioned, in our real scenarios each of our pages (e.g. home and settings) will have their own module that we will load with the router. Once we create individual modules for specific pages/features we will be able to add components like this to the specific feature/module we want to use it in rather than declaring it globally. One step at a time though!
Modify the
HomeComponent
template to reflect the following:
<app-welcome></app-welcome>
<p>I am the home component</p>
Now if we look at our home page:
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):
The reason we see what we see on screen is because:
- Our root component has a
<router-outlet>
in its template - We have a route set up to display the
HomeComponent
in the<router-outlet>
when the path is/home
- We have added our
WelcomeComponent
to the template of ourHomeComponent
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?
Incorrect.
Incorrect. It is @NgModule's that provide a compilation context
Correct!
What is required in order to use the app-root tag in our index.html file?
Incorrect. It is the root module that must be supplied to bootstrapModule
Correct!
The only way to display a component is to configure a route for it with the RouterModule
Incorrect
Correct! We can also add components directly to the template of another component