Angular application based on standalone components with lazy loading and shared elements

Before Angular 14, components must be declared within a module. The module acts as a container for the component and provides a context for it. The module also defines the dependencies that the component requires to work properly. For example, to use certain features such as structural directives, the module must import the relevant module such as the CommonModule. The components declared within a module share the same context and have access to the dependencies imported into the module.

Now with angular 14 and above components can exist as standalone and are no longer required to be declared within a module. Standalone components provide their context by importing the necessary dependencies, making them autonomous.

Components, directives, and pipes can now be standalone, to mark them as standalone you have just to add "standalone: true" into the Component metadata or use the option "--standalone" with angular cli generate command.

Directives and pipes in CommonModule are now marked as standalone and can be imported separately. This migration offers several benefits, including improved tree-shaking. For example, we can now import only the specific directives and pipes that we need, such as NgIf and NgFor, eliminating the need to import the entire CommonModule along with other unnecessary pipes and directives.

import { NgFor, NgIf } from '@angular/common'; import { Component } from '@angular/core';

@Component({
  selector: 'app-root', 
  standalone: true, 
  imports : [NgIf,NgFor], 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.scss'] })   export class AppComponent {

}

Tree-shaking is a feature that reduces the size of the final bundle by including only the code that is used in the application. It’s achieved through static analysis, where unused code is removed, resulting in smaller and faster application builds.

In this article, we will develop an Angular application without any modules.


Initialize Angular Application Without modules

The Angular team is working on developing an ng new collection for applications bootstrapped with a standalone component (https://angular.io/guide/roadmap).

But now we will see how to create a new application and transform it into an application without any modules

Create angular application with angular cli (or you can use Nx)

ng new application-based-standalone 

Would you like to add Angular routing? No 
Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss ]

With Nx, we can create applications directly with standalone components

If you use Angular cli > 15.2.0, Angular offers a schematic to help project authors convert existing projects to the new Standalone API :

Here’s the guide to migrate: https://angular.io/guide/standalone-migration

If you use Nx or Angular CLI(> 15.2.0) migration schematic, you can bypass this section and proceed to the next one, which covers setting up routing with lazy loading.

Otherwise, we will now see how to convert the application manually.

Now, we will transform our application generated by the Angular CLI into an application based on standalone components

  • Add standalone: true into the @Component metadata for AppComponent and import CommonModule

Standalone Component

  • Remove app.module.ts (our application doesn’t need module)

  • Bootstrapping an application using a standalone component

By default, Angular expects a module to bootstrap the application using @angular/platform-browser-dynamic with the function platformBrowserDynamic().bootstrapModule(AppModule). Now, it is possible to give a standalone component for bootstrapping the application using the bootstrapApplication function from @angular/platform-browse.

bootstrapApplication() Bootstraps an instance of an Angular application and renders a standalone component as the application’s root

To bootstrap our application with the AppComponent, we will change main.ts

Before

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';

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

After

import { bootstrapApplication} from '@angular/platform-browser'; import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent) .catch(err => console.error(err));

Run and test application with npm i and ng serve


Setting up routing with lazy loading

The router APIs were updated to take advantage of the standalone components.

In this section, we will create two standalone components, and we will see how to benefit from the updated router API to declare app routes and use lazy loading.

Create two component books and movies

To avoid having to include the --standalone flag each time we generate components, we can set this option directly in the angular.json file since we work with full application standalone components.

"projects": {
    "application-based-standalone": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss",
          "standalone": true
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",

We can now generate our standalone component without the need to include the -standalone flag.

ng generate component books
ng generate component movies

Setting up routes

We will simply create a file called app.routes.ts and define our routes there.

Routes can lazy-load their standalone component by using the loadComponent function.

import { Routes } from "@angular/router";
import { AppComponent } from "./app.component";

export const APP_ROUTES: Routes = [
    {
        path : '',
        children : [
            {
                path: 'books',
                loadComponent: () => import('./books/books.component')
                                          .then(c => c.BooksComponent)
            },
        ]
    }
]

Or we can use loadChildrenthat has been updated to allow loading a new set of child Routes without the requirement to create a lazy-loaded NgModule.

create movies.routes.ts in movies

import { Routes } from "@angular/router";
import { MoviesComponent } from "./movies.component";

export const  MOVIES_ROUTES: Routes = [
 {
    path: '',
    component : MoviesComponent
 }
]

Now we can use lazy loading for children based on routes.

import { Routes } from "@angular/router";
import { AppComponent } from "./app.component";

export const APP_ROUTES: Routes = [
    {
        path : '',
        children : [
            {
                path: 'books',
                loadComponent: () => import('./books/books.component')
                                           .then(c => c.BooksComponent)

            },
            {
                path: 'movies',
                loadChildren: () => import('./movies/movies.routes')
                                            .then(r => r.MOVIES_ROUTES)

            },

        ]
    }
]

We will use the directive RouterOutlet to define a placeholder in the AppComponent which is the parent. The Router displays the activated standalone component in this placeholder by using the selector. To use it, we need to import RouterModule in the context of AppComponent.

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

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

Add <router-outlet></router-outlet> in the template app.component.html

To set up the routes in our application, we use provideRouter() function instead of RouterModule.forRoot.

import { bootstrapApplication} from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router'
import { APP_ROUTES } from './app/app.routes';

bootstrapApplication(AppComponent, 
           {
            providers: [
              provideRouter(APP_ROUTES),
            ]
           })
  .catch(err => console.error(err));

we can see our lazy chunk files when we run ng build

ng build result

You can now run the application and lazy loading can be tested using the URLs /books and /movies. In my example in GitHub, I added a simple button to allow navigation between routes.


Shared elements without module

A shared module enables the centralization and organization of common directives, pipes, and components into a single module, which can be imported as needed in other sections of the application.

In this section, we will learn how to use shared elements without using a module.

  • Create a shared folder within the src/app directory

  • In the src/app/shared directory, create subdirectories for custom shared components (components), custom directives (directives), and custom shared pipes (pipes), in every subdirectory create index.ts (Barrel) file, this file group exported elements

Barrel is an index file that exports multiple other modules, components directives pipes …

  • Create a shared component in the src/app/shared/components

      ng generate component shared/components/shared-cmp
    

shared

  • Export elements in the barrel (index.ts), for example, we need ngIf, and ngFor in the whole application, and re-export them in shared/directives/index.ts (do the same for components, pipes, modules).

      import { Provider } from "@angular/core";
      import { NgIf,NgFor } from "@angular/common";
    
      export const COMMON_DIRECTIVES: Provider[] = [  
          NgFor,
          NgIf,
      ];
    
      import { Provider } from "@angular/core";
      import { SharedCmpComponent } from "./shared-cmp/shared-cmp.component";
    
      export const COMMON_COMPONENTS: Provider[] = [  
          SharedCmpComponent,
      ];
    
      import { Provider } from "@angular/core";
    
      export const COMMON_PIPES: Provider[] = [  
        --- your common pipes
      ];
    
  • Now, let’s gather all of our shared items

Create an index.ts file inside the shared directory

and re-export all our common elements.

import { COMMON_COMPONENTS } from "./components";
import { COMMON_DIRECTIVES } from "./directives";
import { COMMON_PIPES } from "./pipes";

export const SHARED = [
    COMMON_COMPONENTS,
    COMMON_DIRECTIVES,
    COMMON_PIPES,
]

we can now import our shared elements throughout the entire application.

import { Component } from '@angular/core';
import { SHARED } from '../shared';

@Component({
  selector: 'app-movies',
  standalone: true,
  imports: [SHARED],
  templateUrl: './movies.component.html',
  styleUrls: ['./movies.component.scss']
})
export class MoviesComponent {

}

Thanks for reading,if you have questions or comments, let me know in the comments below.The full example can be found on my Github.

To discover more about Angular, JS and Java, you can follow me on Hashnode, Medium or Twitter.