DEV Community

Pranam K
Pranam K

Posted on

Say No to NgModule in Angular 14!

NgModules consolidate components, directives, and pipes into cohesive blocks of functionality, each focused on a feature area, application business domain, workflow, or common collection of utilities.

An NgModule is a class marked by the @NgModule decorator. @NgModule takes a metadata object that describes how to compile a component's template and how to create an injector at runtime. It identifies the module's own components, directives, and pipes, making some of them public, through the exports property, so that external components can use them. @NgModule can also add service providers to the application dependency injectors.

Angular 14 is going to introduce an alternative way to write applications — Standalone components, directives, and pipes.

The term “standalone” refers to components, directives, or pipes that can be used independently of NgModule. Although you’ll still need to use the core and external NgModules, you probably won’t need to create new ones.

Let’s create an application without NgModules. First, we need to generate it using the angular-cli:

npx @angular/cli@next new ng14

The next step is to delete app.module.ts and replace the bootstrapModule() function in main.ts with bootstrapApplication():

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

if (environment.production) {
  enableProdMode();
}

bootstrapApplication(AppComponent)
Enter fullscreen mode Exit fullscreen mode

The bootstrapApplication() can take a list of providers that should be available to the root component and all its children:

import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { HttpClientModule } from '@angular/common/http'

bootstrapApplication(AppComponent, {
  providers: [importProvidersFrom(HttpClientModule)]
}).catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

The function extracts the providers from the provided module.

Now we need to change the AppComponent to be a standalone component. Let's set the standalone property to true:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  standalone: true,
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

Now we can see the AppComponent’s template in our browser. As our component is standalone, we can use the new imports property. The imports property specifies the template dependencies of the component — those directives, components, and pipes it can use.

Standalone components can import other standalone components, directives, pipes, and existing NgModules. For example, we can create a standalone directive, and use it in our component:

npx ng g directive foo --standalone
Enter fullscreen mode Exit fullscreen mode
import { Directive } from '@angular/core';

@Directive({
  selector: '[appFoo]',
  standalone: true
})
export class FooDirective {}
Enter fullscreen mode Exit fullscreen mode
import { CommonModule } from '@angular/common';
import { FooDirective } from './foo.directive';

@Component({
  selector: 'app-root',
  template: `
     <div appFoo *ngIf="bar">Foo</div>
  `,
  standalone: true,
  imports: [FooDirective, CommonModule]
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

Let us add routing to the application.

const routes: Routes = [{
  path: 'todos',
  component: TodosPageComponent
}]

@Component({
  selector: 'app-root',
  template: `
     <a routerLink="/todos">Todos</a>
     <router-outlet></router-outlet>
  `,
  standalone: true,
  imports: [RouterModule.forRoot(routes)],
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

This isn’t possible because Angular doesn’t allow us to use ModuleWithProvider in a standalone component. Next, we might try using the new importProvidersFrom function in the component’s providers:

const routes: Routes = [{
  path: 'todos',
  component: TodosPageComponent
}]

@Component({
  selector: 'app-root',
  template: `
     <a routerLink="/todos">Todos</a>
     <router-outlet></router-outlet>
  `,
  standalone: true,
  providers: importProvidersFrom(RouterModule.forRoot(routes)),
  imports: [FooDirective, CommonModule],
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

Using in-app navigation will work. The router will miss the first navigation. Initialization of the router should be performed in the bootstrap process:

bootstrapApplication(AppComponent, {
  providers: [importProvidersFrom(RouterModule.forRoot(routes))]
}).catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

The TodosPageComponent is eagerly loaded. Let’s change it to load lazily and add a TodoPageComponent:

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

export const todosRoutes: Routes = [
  {
    path: 'todos',
    title: 'Todos Page',
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./todos-page.component').then((m) => m.TodosPageComponent),
        children: [
          {
            path: ':id',
            loadComponent: () =>
              import('./todo-page/todo-page.component').then(
                (m) => m.TodoPageComponent
              ),
          },
        ],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

Instead of using loadChildren and passing a NgModule, we use the loadComponent property and pass a component. We can also declare providers for this Route and its children using the new providers property:

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

export const todosRoutes: Routes = [
  {
    path: 'todos',
    title: 'Todos Page',
    providers: [
      {
        provide: 'Angular',
        useValue: 'v14',
      },
    ],
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./todos-page.component').then((m) => m.TodosPageComponent),
        children: [
          {
            path: ':id',
            loadComponent: () =>
              import('./todo-page/todo-page.component').then(
                (m) => m.TodoPageComponent
              ),
          },
        ],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

We can also pass an array of routes to loadChildren:

export const ROUTES: Route[] = [
  { path: 'child', component: ChildCmp},
]
Enter fullscreen mode Exit fullscreen mode
{
  path: 'parent',
  loadChildren: () => import('./children').then(m => m.ROUTES),
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

In coming days, Angular team may come up with Angular 14 update and new feature's. Stay tuned!

Top comments (3)

Collapse
 
jwhenry3 profile image
Justin Henry

This gives me hope for the future iterations of Angular, but it is still not at the same level of flexibility of react and other frameworks. The component based dependency injection seems really nice, up until the documentation comes into play. So many shifts from the original pattern of Angular that moving to the new pattern will take quite some time for people to get used to it.

Collapse
 
jwp profile image
John Peters

Their ngmodule system is why I left Angular 2 years ago. Won't go back now due to native Webcomponents.

Collapse
 
jwhenry3 profile image
Justin Henry

Also, you might want to add "ts" to your code blocks so you receive syntax highlighting.