DEV Community

paulmojicatech
paulmojicatech

Posted on • Edited on

Angular's Mission To Make DX Better With Standalone Components

This article to highlight Angular 14's new feature for allowing standalone components and how it can be implemented to replace Angular Modules. This feature was delivered to improve developer experience as well as make the framework more approachable for newer developers.

The History

The first iteration of Angular (AngularJs) was a game changer to front end web development. The magic of creating 2 way binding without having to wire up event handlers for elements was an instant win for the community. But the problem was that "magic" could (and most often did) create unmaintainable and non-performant web apps as the complexity of the app grew.

So the Angular team decided to overhaul and re-write from scratch the Angular framework. (This caused a near revolt around the community since AngularJs was heavily adopted and would NOT be compatible with the future versions of Angular, but the Angular team learned from this as well with the ngUpdate feature of the CLI). The re-write would focus heavily on code organization and maintainability of the codebase. This was done by being very opinionated how Angular apps should be written. One of opinions of the Angular team is that using a type system makes things more maintainable and landed on forcing developers of Angular apps to use TypeScript, a subset of Javascript that transpiles the TypeScript code to Javascript.

Another core tenant on maintainability was the ngModule. This concept is that a related set of features should be contained in a module. This, along with the fact that the TypeScript code needed to be transpiled, allowed Angular's compiler to create smaller bundles of Javascript to ship to the browser. But this came at a cost. The compiler needed to read the ngModule file in order to bundle pieces of code together. For instance, a component / directive could not be used unless it was declared in the module. A service could not be injected (more on dependency injection later) into a class unless it was registered (or provided) in the module. The router could lazy load Javascript bundles but the compiler needed to know the module (or Javascript bundle) that needs to be loaded lazily.

In the sections below, we will show how Angular 14 removes the need to create ngModules to accomplish the tasks above. Note: I will be using the term feature as a substitute for ngModule in the sections below to show that the core tenant of maintainability is not lost in the new way to write Angular apps. You still should still create a scaffolding of your app based on features. Most of this is now possible due to the rewrite of Angular's view engine to Ivy. Ivy made it so that the compiler could know with more certainty how to create the Javascript bundles. Ivy is a big topic in itself but just know that a lot of what we can do now in Angular apps is due to Ivy.

Components

Components that are to be used in a feature needed to be declared in the ngModule file like below.

Old Way

   @NgModule({
     imports: [
       CommonModule /* This is an Angular Module that allows what is 
                     exported form this module to be used 
                     in other modules */
     ],
     declarations: [
       MyComponent
     ],
     exports: [ // Anything in this array allows us to use in other modules
       MyComponent
     ]
   })
   export class MyModule(){}

    @Component({
       selector: 'my-comp',
       template: `<div></div>`,
       styleUrls: ['./my-component.component.scss']
    })
    export class MyComponent{}
Enter fullscreen mode Exit fullscreen mode

To use it in a new different feature you would do:

   @NgModule({
     imports: [
       CommonModule,
       MyModule
     ]
   })
   export class NewModule{}
Enter fullscreen mode Exit fullscreen mode

New Way

@Component({
  selector: 'my-comp',
  template: '<div></div>',
  styleUrls: ['./my-component.component.scss'],
  standalone: 'true',
  imports: [
    CommonModule
  ],
  exportAs: 'my-comp'
})
export class MyComponent(){}
Enter fullscreen mode Exit fullscreen mode

To use it from a legacy module you could do:

@NgModule({
  imports: [
    CommonModule,
    MyComponent
  ]
})
export class NewModule{}
Enter fullscreen mode Exit fullscreen mode

To use it from another standalone component you could do:

@Component({
  ...,
  imports: [
    CommonModule,
    MyComponent
  ],
  standalone: true
})
export class NewComponent
Enter fullscreen mode Exit fullscreen mode

Dependency Injection And Services

Angular leaned heavily into using dependency injection as a core tenant of Angular. Dependency injection provides a way to logically separate the concerns of a codebase to make your apps more maintainable and testable.

In Angular, the instance of a class that is injected into a another class is must be registered. This can be done at the app, module, or component level. For instance, if the service is registered in the app.module.ts file, which is the module that is bootstrapped when the application started, the class registered in the app module would be a singleton. This means that there is only one instance of the class running in the application.

Angular strongly leans towards making an injectable class as a singleton so they create a way to register the class within the class. To do so, you can simply register the class by

@Injectable({
  providedIn: 'root'
})
export class MyService{}
Enter fullscreen mode Exit fullscreen mode

Routing

We talked about the ability to lazy load modules (or Javascript bundles) only when the code is really needed. This is done through Angular's routing system. Since before we were using modules to group together code, Angular could derive when to actually send that bundle down to the browser to use based on if a user navigated to a certain route. Below we show the old way to do it, followed by how to do it in Angular 14 with standalone components.

Old Way

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


app.module.ts
const routes: Route[] = [
  {
    'path': 'my-feature',
    'loadChildren': () => import('./my-feature/my-feature.module').then(m => m.MyModule)
  },
  {
    'path': '',
    redirectTo: 'my-feature'
  }
];
@NgModule({
  imports: [
    RouterModule.forRoot(routes),
    StoreModule.forRoot({})
  ]
})
export class AppModule{}


my-feature.ts
const routes: Route[] = [
  {
    path: 'comp-one',
    component: ComponentOne
  },
  {
    path: '',
    component: ComponentTwo,
    pathMatch: 'full'
  }
];
@NgModule({
  imports: [
    CommonModule,
    RouterModule.forFeature(routes),
    StoreModule.forFeature('my-feature', featureReducer)
  ]
})
export class FeatureOne{}
Enter fullscreen mode Exit fullscreen mode

New Way


my-feature.routes.ts
const MY_FEATURE_ROUTES: Route[] = [
  {
    path: 'comp-one',
    loadComponent: () => import('./components/comp-one.component.ts').then(c => c.ComponentOne)
  },
  {
    path: '',
    loadComponent: () => import('./components/comp-two.component.ts’).then(c => c.ComponentTwo),
    pathMatch: 'full'
  }
];

main.ts

const routes: Route[] = [
  {
    path: 'my-feature',
    loadChildren: () => import('./my-feature.routes.ts').then(m => m.MY_FEATURE_ROUTES),
    providers: [
      importProvidersFrom(
        StoreModule.forFeature('my-feature', featureReducer)
      )
    ]
  }
];

bootstrapApplication(
  AppComponent,
  {
     providers: [
       importProvidersFrom(
         RouterModule.forRoot(routes),
         StoreModule.forRoot({}),
         EffectsModule.forRoot([]),
         StoreDevtoolsModule.instrument({}),
         HttpClientModule,
         BrowserAnimationsModule
       )
    ]
  }
)
Enter fullscreen mode Exit fullscreen mode

And that's it. Now we do not need any modules in our Angular apps! Happy coding!

Follow me on BlueSky

Top comments (0)