This year instead of posting weekly, I'm trying to get fewer more in depth blog posts. Here is one all about Angular stand alone components.
Angular 14 introduced the concept of a stand-alone component. A stand-alone component is one that does not need a module. These are designed to help simplify the boiler plate when building Angular applications, and to make them more accessible to users.
Create Your first Standalone Component
To start with, I created a new Angular 15 project with the Angular CLI. From there, I ran my first command:
We're asking the Angular CLI to generate a component named view1. You've seen this before if you got this far in the series, but let's dissect each section of the command:
- ng: This is the Node hook to the Angular CLI.
- generate: This is the command you send to the Angular CLI to tell it to create some entity.
- component: This referrers to the type of entity you want the angular CLI to generate.
- view1: This is the name of the component you want to create.
- --standalone: This parameter tells the Angular CLI to generate a standalone component, not a normal component. This is the new flag.
Run this command and you'll see something like this:
This response is pretty common from generating other components, but you will notice that the main app.module.ts file is not updated. This is because standalone components do not need to be added to the module.
Let's look at the generated component code. First, open the src/app/view1/view1.component.ts file:
2import { CommonModule } from '@angular/common';
3
4@Component({
5 selector: 'app-view1',
6 standalone: true,
7 imports: [CommonModule],
8 templateUrl: './view1.component.html',
9 styleUrls: ['./view1.component.css']
10})
11export class View1Component {
12}
Let's take a look at the configuration object in the @component annotation:
- selector: This specifies how we'll use the component in a view. You're used to this from traditional components.
- standalone: This is a new property that tells Angular that this component is a standalone component. The value is true or false. I believe it is false by default. We can also use this new flag on directives and pipes, in addition to components.
- Imports: This is a new property for components, and it is modeled after the same property on a module. This specifies the other dependencies that this component has. By default we're importing the CommonModule, which contain the fundamentals of the Angular framework. You may need to import other sub components here that you want to use.
- templateUrls: This is legacy and specifies the HTML template that belongs to this component. Traditional components already use this property.
- styleUrls: This is an array of related CSS files that belong to this component. This one is also legacy and you should be familiar with it.
Three other files were created. The view1.component.css will start out as an empty file. The view1.component.spec.ts is a testing file, and we'll loop back to it later in this article. The view.component.html is the template file:
It is as simple as you'd expect.
Using the Component
Now that we've created the component, how do we add it to a view? Our main application is still module based, so we have to import it into the module. Open up app.module.ts:
Add the View1Component to the import list:
2 BrowserModule,
3 AppRoutingModule,
4 View1Component
5],
And be sure to import it:
Now, open up the app.component.html file, remove all the default contents, and add it:
Run the development server:
You should see something like this:
This isn't different than what you're used to seeing at the command line.
Point your browser over to localhost:4200:
Congratulations! You've created your first standalone component.
What surprised me about the learning is that the main module still exists, and it is required that you import the component to use it. It looks like the component just moved from the declarations to the imports section. There is a way to load an application without specifying a main module, but you'll have to get my Coding Bonus Book on Angular 15 for my sample.
Using Multiple Components
Let's jump into a sample where multiple standalone components work together. One of my favorite examples when demonstrating components, is to create one component that modifies a piece of data and another component that displays it. I'm starting this sample with a brand-new project, creating using `ng new`.
First, let's generate the view component:
You should see something like this:
This component will accept a single input, and display it in the view template.
Let's look at the component default state. Open the display.component.ts file
2import { CommonModule } from '@angular/common';
3
4@Component({
5 selector: 'app-display',
6 standalone: true,
7 imports: [CommonModule],
8 templateUrl: './display.component.html',
9 styleUrls: ['./display.component.css']
10})
11export class DisplayComponent {
12}
Inside the class, add an @input property:
Don't forget to update the imports:
Now, pop open the display.component.html file:
Replace all of this with:
This component should accept an input, an display it. Super simple!
Now, let's generate a component to edit a value:
You should see this:
Let's take a look at the first:
2import { CommonModule } from '@angular/common';
3@Component({
4 selector: 'app-edit',
5 standalone: true,
6 imports: [CommonModule],
7 templateUrl: './edit.component.html',
8 styleUrls: ['./edit.component.css']
9})
10export class EditComponent {
11}
For simplicity, we're going to use ngModel to attach an input to a variable, and to do that we need to import the FormsModule. In the old school way, we'd do that as part of an angular module, but here we can add it directly to the imports config of the component:
Don't forget to import it into the class:
Now let's add a variable:
And an output event:
2valueToEditChanged: EventEmitter<string> = new EventEmitter<string>();
I named the output event valueToEditChanged, to mirror the valueToEdit. Make sure to add the imports for Output and EventEmitter:
Open up the edit.component.html file and you'll see this:
Replace it with this:
2 [(ngModel)]="valueToEdit"
3 (ngModelChange)="onValueChange()">
Just a single input of the type "text". It uses ngModel with double binding to keep track of the component. It also uses the ngModelChange event to make a call to an onValueChange() method. Switch back to the edit.component.ts file to create the method:
2 this.valueToEditChanged.emit(this.valueToEdit);
3}
This method simply dispatches the event, passing in the new value.
We've created two components, be sure to import them both into the app.module.ts:
2 BrowserModule,
3 AppRoutingModule,
4 DisplayComponent,
5 EditComponent
6],
And don't forget your TypeScript imports:
2import { EditComponent } from "./edit/edit.component";
We're almost ready. Now we open up the app.component.html file. Delete all its contents, and load in our two components:
2<app-edit (valueToEditChanged)="display.valueToDisplay = $event"></app-edit>
3
4<h1>Display</h1>
5<app-display #display ></app-display>
First comes the edit component. It listens to the valueToEditChanged event and runs an in-line function. The inline function makes a change to the display component, tweaking the valueToDisplay property. The display component doesn't have any formal properties set, however I did give it the name display using the #display syntax.
I'm setting up the component communication in the HTML for simplicity in this sample. Normally I try to avoid adding any sort of business logic--even simple logic--in the HTML, because it makes things harder to test.
Go back to your console, run ng serve and take a look at the browser:
The default state has no text entered, and therefore displays no text. Start typing and you should see the display automatically update:
All good!
How to use Nested Standalone Components
It is very common in a real-world app for one component to be nested inside another. Can we do this with standalone components? Absolutely! Let's create a new component that wraps our display and edit components. For this, I'm going to continue to iterate over the previous sample.
Let's start by creating a standalone wrapper component:
It'll look like this:
Let's open up the wrapper.component.ts:
2import { CommonModule } from '@angular/common';
3@Component({
4 selector: 'app-wrapper',
5 standalone: true,
6 imports: [CommonModule],
7 templateUrl: './wrapper.component.html',
8 styleUrls: ['./wrapper.component.css']
9})
10export class WrapperComponent {
11}
We want to import our other two standalone components as part of the Component annotation configuration object:
Be sure to import the classes for TypeScript:
Now, open the wrapper.component.html:
Delete all this, and replace it with what we had added to the app.component.html previously:
2<app-edit></app-edit>
3
4<h1>Display</h1>
5<app-display></app-display>
Now, let's open the app.component.html file, and you'll still have the above. Replace it with this:
Let's open up the app.module.ts file. Previous we had imported the DisplayComponent, and EditComponent as part of the module:
2 BrowserModule,
3 AppRoutingModule,
4 DisplayComponent,
5 EditComponent
6],
However, we no longer need to do that, since they are only used as children to the WrapperComponent, so let's remove those and add the Wrapper:
2 BrowserModule,
3 AppRoutingModule,
4 WrapperComponent
5],
Don't forget about the TypeScript import:
From here, we should be able to re-run the code and see this working. Point your browser at localhost:4200 after restarting your dev server:
Start typing and see the updates:
I reused the images from earlier, but trust me these work.
Routes and Lazy Loading
My favorite use case of stand-alone components is to lazy load elements through the router. In the past, if we wanted to lazy load a component or section of an app, we'd have to point it to a module. You'd have to do something like this:
2 {
3 path: `subsection`,
4 loadChildren: () => import(`./subsection/subsection.module`).then(
5 m => m.SubsectionModule
6 )
7 }
8];
The module would have to load all components, providers, and could even include it's own router. I always found this to be a lot of boilerplate code just to implement lazy loading. Now with a standalone component, we can make it simpler, which I love.
First, let's create a simple routes that does not include lazy loading. Open up the app-routing.module.ts file:
2 { path: 'view1', component: WrapperComponent },
3 { path: '**', redirectTo: 'view1' }
4];
The first route is named view1, and points to WrapperComponent. I also added a catch all route, distinguished by the two asterisks in the path. The catch all route redirects to the view1.
Now, open up app.component.html:
Remove this and replace it with the router outlet:
With this approach, we can remove the component import from the app.module.ts. Instead of this:
2 BrowserModule,
3 AppRoutingModule,
4 WrapperComponent
5],
We can just do this:
2 BrowserModule,
3 AppRoutingModule
4],
Recompile the app, and load it in the browser:
You can see the route in the browser at view1. Start typing to make sure it all works:
All good to go; so, we've proved that the stand-alone components are working fine with the router. But, the use of the router here mirrors what we did in the main books, which did not investigate lazy loading at all. How do we set up lazy loading of the module?
Back to the app-routing.module.ts and replace the view1 route with this:
2 path: `view1`,
3 loadComponent: () => import(`./wrapper/wrapper.component`).then(
4 m => m.WrapperComponent
5 )
6},
The syntax is very similar to the lazy loading segment I shared earlier. Instead of a loadChildren() method, we now use a loadComponent() method. The value is an arrow functions. The arrow function uses the import command to load in the component. The import returns, surprisingly, a promise. We resolve the promise with the then() method, and the result handler returns the component.
You can re-run the app right now to see that it is continuing to work, but to see the real power of lazy loading, let's create another route and view. Let's create a new component:
You'll see something like this:
We're not going to implement a lot of functionality in the View2Component, only using it to link between the two respective views. Take a look at the default view2.component.ts:
2import { CommonModule } from '@angular/common';
3@Component({
4 selector: 'app-view2',
5 standalone: true,
6 imports: [CommonModule],
7 templateUrl: './view2.component.html',
8 styleUrls: ['./view2.component.css']
9})
10export class View2Component {
11}
To tell Angular to link between routes, we'll need to load in the RouterModule as part of the imports configuration:
And don't forget the TypeScript import:
Open up view2.component.html. Replace all the contents with a link back to View1:
For View2Component that's all we need.
Let's go back to View1 which is powered by WrapperComponent. First, we'll need to tell it about the RouterModule in the component config imports:
Don't forget the TypeScript import:
This is the same as what we did in the View2Component. We'll made a similar addition in the wrapper.component.html:
I added a link to view2 in front of the other two components in the WrapperComponent.
Now, open up the app-routing.module.ts. I'm going to show you the two cases, and how the browser interacts differently. First, with no lazy loading setup:
2 {
3 path: 'view1', component: WrapperComponent
4 },
5 {
6 path: 'view2', component: View2Component
7 },
8 { path: '**', redirectTo: 'view1'}
9];
Run the app, and take a look at the network console:
Click in the browser between the two views and you'll notice nothing changes in the network tab. Everything is loaded all at once.
Now, let's switch back to lazy loading in the app-routing.module.ts:
2 {
3 path: `view1`,
4 loadComponent: () => import(`./wrapper/wrapper.component`).then(
5 m => m.WrapperComponent
6 )
7 },
8 {
9 path: `view2`,
10 loadComponent: () => import(`./view2/view2.component`).then(
11 m => m.View2Component
12 )
13 },
14 { path: '**', redirectTo: 'view1' }
15];
We've already explored the view1 route and the catch all redirect route. This includes a view2 route, which loads the View2Component. Now reload the app in the browser and review the network tab:
We load most of the same files. One thing you'll notice is that main.js is a lot smaller. And there is also a an additional js file loaded, which corresponds to the WrapperComponent. The end result is that we have less data loaded on the initial app load--which means your users are using the application much quicker.
Click over to View2:
I didn't clear out the network tab before clicking, but primarily you see View2Component loaded, but not until we clicked on the link to load that view.
It is beyond the scope of this article to go into the benefits and specifics of lazy loading, but let's just say I love this use case for stand-alone components, because it is something that clearly removes a lot of boilerplate.
Final Thoughts
I learned a lot by exploring this post, and writing the chapter in my book. I hope you learned a lot reading it. I'm not sure how I feel about standalone components yet, but I do see them as a one way to make Angular applications easier to build, especially around lazy loading. I also see a lot of potential using them in libraries, where you want to optimize a component for reuse.
If you've gotten this far, thank you for reading. Be sure to check out the bonus book from the Learn With Series on Angular 15 , which includes a more in depth version of this post with more samples including using services with standalone components, bootstrapping an application without using a module, and learning how to test stand alone components.