angular · GitHub

Author: Pawel Kozlowski
Contributors: Alex Rickabaugh, Andrew Kushnir, Igor Minar, Minko Gechev, Pete Bacon Darwin
Area: Angular Framework
Posted: October 8, 2021
Status: Complete - outcome summary linked here.

The goal of this RFC is to validate the design with the community, solicit feedback on open questions, and enable experimentation via a non-production-ready prototype included in this proposal.

Motivation

NgModule is currently one of the core concepts in Angular. Developers new to Angular need to learn about this concept before creating even the simplest possible "Hello, World" application.

More importantly, NgModule acts as a "unit of reasoning and reuse":

  • libraries publish NgModules
  • lazy-loading is centered around NgModule, etc.

Given this central role of NgModule in Angular it is hard to reason about components, directives and pipes in isolation.

Dynamic component creation example

The following example, which dynamically renders a component, contains a subtle, yet critical, problem and is not guaranteed to work at runtime as a result!:

import {Component, ViewContainerRef} from '@angular/core';
import {UserViewComponent} from './business-logic';
@Component({...})
class DynamicUserView {
    constructor(private viewContainerRef: ViewContainerRef) {}
    renderUserView(): void {
      this.viewContainerRef.createComponent(UserViewComponent);
    }
}

Suppose UserViewComponent is authored like this:

@Component({...})
export class UserViewComponent {
  constructor(readonly service: UserViewService) {}
}
@NgModule({
 declarations: [UserViewComponent],
  imports: [/* dependencies here */],
  providers: [{provide: UserViewService, useClass: BackendUserViewService}],
})
export class UserViewModule {}

UserViewComponent here assumes it will be able to inject UserViewService. This assumption is usually safe because in ordinary usage, users who want to use UserViewComponent don’t depend on it directly, but instead add UserViewModule to their NgModule.imports. UserViewModule brings with it the provider needed for UserViewService, and so the component will work just fine.

Attempting to instantiate UserViewComponent directly, however, risks violating this assumption. If the application hasn’t independently imported UserViewModule somewhere in its NgModule hierarchy, the needed provider won’t be available at runtime, and dynamic creation will fail.

Some components do not have such dependencies — they don’t rely on configuration provided in their NgModule — and can be used directly in this manner. Many components, however, do rely on the context provided by their NgModule, either its providers, or by expecting certain other components or directives to also be present in the same template.

Components need NgModules

This may seem like an implementation detail of specific components, but in fact it illustrates a fundamental property of the framework: NgModules are the smallest reusable building blocks in Angular, not components.

Angular is one of the only web frameworks where components are not the “units of reuse”.

Having Angular conceptually centered around NgModule has a significant impact on the developer experience:

  • authoring components is more involved than coding a class and a template, since:
    • the component might need to be in its own NgModule, if it’s meant to be reused independently, or
    • the author must fit the component somewhere else in the application’s NgModule hierarchy.
  • APIs around loading and rendering components are:
    • unnecessarily complex, e.g. bootstrapModule() vs bootstrapComponent(), or
    • easy to misuse, like in the ViewContainerRef.createComponent() example above.
  • reading component code isn’t sufficient to understand the component behavior:
    • a reader must track down the component’s NgModule to understand the component’s dependencies.
  • Angular’s tooling must deal with the “implicit” dependencies of components on their NgModule context:
    • this negatively affects both build performance and the optimizability of our generated code.

Main benefits of this proposal

Move Angular in a direction where components, directives, and pipes play a more central role, are self-contained and can be safely imported / used directly.

  • simplifies the mental model of Angular
  • makes new APIs for using components and directives possible (such as fine-grained lazy loading)
  • improves the ability of Angular tooling to process code efficiently.

The mental model shift is the main motivation of this proposal, but there are additional benefits of the reduced conceptual surface (fewer things to learn) and API surface (less code to write).

All these benefits combined should make Angular:

  • simpler to use,
  • easier to reason about,
  • less verbose to write, and
  • faster to compile (more details in #43165).

Goals and non-goals

Goals

  • Shift Angular towards a simpler reuse model that isn’t centered around NgModule:
    • allow for a simpler model where components, directives and pipes are self-contained and can be consumed directly;
    • make it possible to introduce new APIs around more dynamic usages of components, directives, pipes;
    • ensure that Angular code written in this style is easier to read and reason about;
    • ensure that Angular code written in this style is more easily processed and optimized via tooling.
  • Improve the developer experience:
    • new Angular users don’t encounter NgModule.declarations until much later in their education;
    • allow components, directives and pipes to be written without needing accompanying NgModules, reducing the amount of code that needs to be written for typical development scenarios;
    • enable applications where the NgModule concept and API is not needed at all, and as such doesn't need to be learned / mastered.
  • Minimize impact on the Angular ecosystem:
    • overall mental model: developers should not have to learn a new set of rules to reason about their applications using standalone components, directives and pipes;
    • "don't break the World": existing libraries should work as-is without any additional changes;
    • existing documentation and training materials should not become invalid as the result of this proposal;
    • interoperability: standalone components should be able to use existing libraries and standalone components should be usable in existing NgModule-based applications.
  • A neutral impact on performance metrics:
    • code size: applications written with standalone components should not be any larger than their NgModule-based counterparts;
    • runtime performance: applications using standalone components should not be slower as compared to their NgModule-based counterparts;
    • compilation time should not increase with the adoption of standalone components — on the contrary, we expect to see improved incremental compilation times for applications opting into standalone components, directives and pipes.

Non-Goals

This proposal is not trying to remove the concept of a NgModule from Angular — it is rather making it optional for typical application development tasks.

At the same time we believe that it paves the path towards greatly reducing the role of NgModule for typical development scenarios — to the point that some time in the future it would be possible and reasonable for us to consider removing it altogether.

Proposal

Current state

In Angular today, developers use NgModules to manage dependencies. When one component needs to make use of another component, directive, pipe or a provider (whether from within the same application, or from a third-party library on NPM) the dependency is not referenced directly. Instead, an NgModule is imported, which contains exported components, directives and pipes as well as configured providers.

Depending on things indirectly via an imported NgModule introduces subtle assumptions:

  • configuration of the required dependency injection (DI) tokens: because the application is required to import the NgModule in order to use the component, directive or pipe, any providers declared within that NgModule are guaranteed to be available for injection. If the application were somehow able to skip the NgModule and depend on a component directly, there is no guarantee that the DI system would be correctly configured and be able to instantiate the component;

  • declarations of collaborating directives: a directive may require other directives to also match where it is used, even if the end user isn’t aware of their existence.

Collaborating directives example

When the NgModel directive matches on an <input> element like so:

<input type="text" [(ngModel)]="twoWayBoundExpr">

it also expects the collaborating DefaultControlValueAccessor directive to match on the same <input> DOM element (through the input selector).

The FormsModule (which exports NgModel) also exports DefaultControlValueAccessor.

Both the NgModel and the DefaultControlValueAccessor directives must be active on the element for [(ngModel)] to function properly.

Most Forms users are entirely unaware of this mechanism.

Generally speaking components, directives or pipes declared in a NgModule assume presence of a certain context (DI tokens and collaborating directives). An NgModule specifies this context.

Standalone components, directives, and pipes

A standalone directive, component, or pipe is not declared in any existing NgModule, and:

  • directly manages its own dependencies (instead of having them managed by an NgModule);
  • can be depended upon directly, without the need for an intermediate NgModule.

The standalone flag is used to mark the component, directive or pipe as "standalone". It is a property of a metadata object of the relevant decorator (@Component, @Directive, or @Pipe).

ℹ️ Adding the standalone flag is a signal that components, directives, or pipes are independently usable. Such components, directives, or pipes don't depend on any "intermediate context" of a NgModule.

Simple example

Let's examine a simple example:

import {Component} from '@angular/core';
@Component({
  standalone: true,
  template: `I'm a standalone component!`
})
export class HelloStandaloneComponent {}

In HelloStandaloneComponent, the standalone: true flag marks the component as standalone. This makes it obvious to the reader and the tooling that this component is self-contained:

  • it does not depend on any "hidden context";
  • it can be used directly, and
  • it cannot be declared in any NgModule.

Dependencies example

Since a standalone component has no association with an NgModule, we need a different mechanism of specifying template dependencies. The imports property on the decorator specifies the component's template dependencies — those directives, components, and pipes that can be used within its template:

import {Component} from '@angular/core';
import {FooComponent, BarDirective, BazPipe} from './template-deps';
@Component({
  standalone: true,
  imports: [FooComponent, BarDirective, BazPipe],
  template: `
    <foo-cmp></foo-cmp>
    <div bar>{{expr | baz}}</div>
  `
})
export class ExampleStandaloneComponent {}

Interop with NgModule examples

Standalone components, directives and pipes can be imported by other standalone components, as well as by NgModules:

@NgModule({
  declarations: [AppComponent],
  imports: [ExampleStandaloneComponent],
})
export class AppModule {}

Here, AppComponent (which is declared in AppModule and thus has its template managed by AppModule) is given visibility of ExampleStandaloneComponent via the import in AppModule.

Conversely, standalone components can also import existing NgModules:

@Component({
  standalone: true,
  imports: [CommonModule],
  template: `
    <div *ngFor="let user of users$ | async">{{user.name}}</div>
  `
})
export class ExampleStandaloneComponent {}

In this example, ExampleStandaloneComponent uses the NgForOf directive and the AsyncPipe, both of which are made available by importing CommonModule. This ability of importing existing NgModules is very important for the interoperability story — it assures that the large ecosystem of existing NgModules is usable as-is from standalone components.

Ways to reason about standalone components

⚠️ Experienced Angular developers might find it easier to reason about the design by using one of the analogies to NgModule described in this section. If you are new to Angular or the NgModule concept you can safely skip this part of the RFC and go directly to the "Use-cases and code examples" part.

Virtual NgModule mental model

A standalone component, directive, or pipe can be considered as being self-declaring. They behave as if there was an NgModule which declared (and exported) the component in question (and only this one component). In practice this NgModule doesn't exist (or is not made accessible to developers), and can be thought of as "virtual".

Considering an example standalone component:

@Component({
  standalone: true,
  imports: [CommonModule],
  template: `
    <ng-template [ngIf]="show">
        I'm shown!
    </ng-template>
  `
})
export class ExampleStandaloneComponent {
    @Input show;
}

This component will behave as if it was declared and exported from a "virtual" NgModule:

@NgModule({
  declarations: [ExampleStandaloneComponent],
  imports: [CommonModule],
  exports: [ExampleStandaloneComponent]
})
export class ExampleStandaloneComponent {
}

Please note that we are using the same name (ExampleStandaloneComponent) to indicate that a standalone component takes on some responsibilities of a @NgModule. It is also a hint that we will not generate a "virtual" @NgModule class in the final implementation.

As previously mentioned, this "virtual" NgModule is not accessible to developers, and the ExampleStandaloneComponent class can be used in its place throughout Angular.

SCAM pattern mental model

Another way of thinking about standalone components, directives and pipes is using the analogy of a single-component Angular module (so-called SCAM pattern popularized by @LayZeeDK). With this proposal an NgModule for a single component, directive, or pipe does not have to be written by a developer — it is "natively supported" by the framework.

⚠️ The "virtual" NgModule or the SCAM pattern is just a "thinking tool" to help us reason about the design described in this RFC. For performance and maintainability reasons, the actual implementation of this proposal will very likely not end up generating or using "virtual" / SCAM NgModules.

Declarations

Declaring a standalone component, directive or pipe in an NgModule is an error reported at compilation time.

ℹ️ Virtual NgModule analogy:
A standalone component, directive or pipe was already declared in its own "virtual" NgModule and it is not possible to declare a component, directive or pipe in 2 different NgModules.

Imports and schemas

Since a standalone component takes on some responsibilities of a NgModule we need to extend the list of the properties available in the @Component decorator. More formally we add the following properties with the same syntax and semantics as if placed in an @NgModule:

The imports and schemas properties on the @Component annotation are allowed only in the presence of the standalone: true flag. The compiler will report an error if imports or schemas property is present without the associated standalone: true flag.

ℹ️ Virtual NgModule analogy:
Importing a standalone component/directive/pipe into either another standalone component, or into an NgModule, behaves as if its virtual NgModule was imported instead. This means that the single exported standalone component, directive or pipe is added to the compilation scope of the importing NgModule.

Providers from imported NgModules

Existing NgModules imported into a standalone component might contain providers.

The providers of all NgModules imported (directly or transitively) into a standalone component are "rolled up" and made available to other NgModules or standalone components that import it in turn.

This "rolling up" of providers continues until we reach a top-level NgModule (typically the application NgModule but potentially a lazy-loaded NgModule).

This is actually how providers are handled in the NgModule imports graph today: Providers are not scoped to an instance of a NgModule but rather are instantiated by an injector representing an accumulated set of all providers from the entire imports graph.

ℹ️ Virtual NgModule analogy:

The collection of providers can be illustrated on the following drawing:

The mechanism is equivalent to how providers are interpreted when traversing the NgModule imports graph today - the only difference here is that the imports graph can contain a mix of "real" NgModule (hand-written by Angular developers) and "virtual" ones (representing standalone components, directives or pipes).

Component providers

Unlike providers that are "rolled up" from imported NgModules, providers declared via the providers property on a standalone component keep the same semantics as a non-standalone component.

In practice this means such providers are defined on the node injector associated with the host node of the component and not an NgModule or top level application injector.

Instead, to ensure a provider is added to a top level injector, a standalone component, directive or pipe should use tree-shakeable providers - for example @Injectable({providedIn: 'root'}).

Unlike a real NgModule, a standalone component (and its "virtual" NgModule) can NOT specify providers to be instantiated on a top level injector.

Use-cases and code examples

This section goes over several practical use-cases and provides code examples for each use-case. Here we don't introduce any new concepts nor APIs but rather "derive" them from the fundamental design choices outlined so far.

@component / @directive / @pipe APIs

Standalone components, directives and pipes

A component, directive, or pipe can be marked as "standalone". This clearly signals that the “standalone” entity is not declared in any NgModule and thus is not part of any NgModule.

Example component:

@Component({
  selector: 'first-standalone-component',
  standalone: true,
  template: `I'm first!`
})
export class FirstStandaloneComponent {
}

Example directive:

@Directive({
  selector: '[standaloneRedBorder]',
  standalone: true,
  host: {
    style: 'border: 2px dashed red'
  }
})
export class StandaloneRedBorderDirective {}

Example pipe:

@Pipe({
  name: 'standaloneStar',
  standalone: true
})
export class StandaloneStarPipe implements PipeTransform {
  transform(value) {
    const stars = new Array(value.length);
    return stars.fill('*').join('');
  }
}

Notable points:

  • the same concept of "standalone" applies to components, directives and pipes;
  • the same syntax (standalone: true) is used to mark a component, directive or pipe as standalone.

Standalone components using custom elements

Standalone components can use custom elements in a template by specifying an appropriate element name validation schema, ex.:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
  selector: 'using-ce-component',
  standalone: true,
  template: `<custom-element></custom-element>`,
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class UsingCustomElementsComponent {
}

Standalone component with template dependencies

Standalone components are not declared in any NgModule but still need a way of specifying their template dependencies. This is done with the imports property of the @Component decorator:

import { FirstStandaloneComponent } from './firstStandalone.component';
@Component({
  selector: 'standalone-importing-standalone-component',
  standalone: true,
  imports: [FirstStandaloneComponent],
  template: `
    Turtles all the way down:
    <first-standalone-component></first-standalone-component>
  `
})
export class StandaloneImportingStandaloneComponent {} 

It is also possible to directly depend on components, directives and pipes exported by existing NgModules:

import { FormsModule } from '@angular/forms';
@Component({
  selector: 'standalone-with-import-component',
  standalone: true,
  imports: [FormsModule],
  template: `
    Forms work: <input [(ngModel)]="name" /> (name = {{ name }})
  `
})
export class StandaloneWithImportComponent {
  name = 'Daft Punk';
}

The @Component.imports supports the same syntax and semantics as @NgModule.imports. In practice it means that users could group several collaborating directives in an Array and use such group in @Component.imports:

// imports const COLLABORATING_DIRECTIVES = [DirectiveFoo, DirectiveBar];
import { COLLABORATING_DIRECTIVES } from './collaborating-group';
@Component({
  selector: 'standalone-importing-standalone-component',
  standalone: true,
  imports: [COLLABORATING_DIRECTIVES],
  template: `<div [foo]="exp" [bar]="exp"></div>`
})
export class StandaloneImportingStandaloneComponent {} 

This technique makes it possible to create groups of collaborating standalone components, directives and pipes (ones that should match together on a given element) without needing an NgModule.

Notable points:

  • standalone components can depend on existing NgModules (no changes are required to those modules) - this means that standalone components can take advantage of the entire existing ecosystem of libraries exposed as NgModules;
  • with the "virtual NgModule" mental model we can think of the imports property as "import NgModule"s here. There is really no distinction between importing an existing NgModule and a standalone component, directive or pipe - we always import an NgModule (a "real" or a "virtual" one);
  • the imports property has the same syntax and semantics as the same property on the @NgModule decorator. Most notably, the value of this property must be statically analyzable.

Libraries

With the introduction of standalone components, directives and pipes we open up a debate on changes to how libraries should be architected in response to this proposal. Essentially a library author will have the following choices:

  1. export an NgModule only;
  2. export both standalone components, directives, and pipes, as well as an "aggregating" NgModule (for applications that prefer to use them);
  3. export a set of standalone components, directives, and pipes only.

Regardless of the final recommendation and the exact choice done by the library author it is important to note that all the options listed above are possible.

To start with, library authors can continue to publish the existing NgModule without any changes. Those are guaranteed to work as-is. This is also the best choice for libraries composed of collaborating directives that must match on the same element (the NgModel + DefaultValueAccessor combination is a good example).

Then, a library might choose to expose standalone components, directives and pipes but still create a NgModule:

@Directive({
  selector: '[blueBorder]',
  standalone: true,
  host: {
    style: 'border: 2px dashed blue'
  }
})
export class BlueBorderDirective {}
@Pipe({
  name: 'blackHole',
  standalone: true
})
export class BlackHolePipe implements PipeTransform {
  transform(value) {
    return '';
  }
}
// backward-compatibility NgModule
@NgModule({
  exports: [BlueBorderDirective, BlackHolePipe]
})
export class LibModule {}

Finally, a library author might choose to export exclusively a set of standalone components, directives and pipes. Such a set would be usable from standalone components and could be imported into any NgModule (if an application chooses to use them). This is a good choice when a library consists of independent and non-cooperating components, directives and pipes.

Notable points:

  • libraries are free to choose how to export their deliverables, and should choose the approach that "makes most sense" given the expected usage patterns;
  • a library might choose to export standalone components, directives and pipes individually yet still provide an NgModule for applications that prefer to import a library as the "whole";

Other APIs using NgModule

The "standalone" concept makes it possible to simplify the existing APIs: generally speaking it should be possible to use a standalone component, directive or pipe in places where an NgModule was previously required.

This section contains examples of APIs that could be simplified. The intention is to show what might be possible rather than fully design or commit to those APIs.

Components: lazy loading and instantiation

At present, when lazy loading components in Angular, developers have to first lazy-load the component's NgModule, and then use it to instantiate the component. The NgModule context is required to ensure that declared components have their compilation scope and providers setup correctly.

With the "standalone" option we've got a guarantee that a component is "self-contained" and we can start using standalone components as a lazy-loading boundary:

@Component({
  selector: 'app-component',
  template: 'dynamically loaded: '
})
export class AppComponent {
  constructor(private vcRef: ViewContainerRef) { }
  ngOnInit() {
    import('./path/to/component').then(m => {
       this.vcRef.createComponent(m.StandaloneComponent);
    });
  }
}

Notable points:

  • the StandaloneComponent can be lazy-loaded without any associated NgModule;
  • a lazy-loaded component can be instantiated in a view container as any other component.

Bootstrap

Angular developers need to create an NgModule in order to bootstrap even the simplest "Hello, World" application. In practice this means that the NgModule concept needs to be taught and learned while getting started with Angular.

With the "standalone" proposal implemented, we could introduce an alternative bootstrap API where a standalone component is directly used as a root component:

import { Component, bootstrapComponent } from '@angular/core';
@Component({
    selector: 'hello-world',
    standalone: true,
    template: 'Hello, World!'
})
export class HelloWorldComponent {}
bootstrapComponent(HelloWorldComponent);

Notable points:

  • applications using "standalone components" could be written without the need to learn about the NgModule concept and the associated APIs.

Router

Since a standalone component can be lazy loaded and dynamically instantiated we could modify router APIs to allow standalone, lazy-loaded leaf routes without the need for child route configuration or an NgModule:

RouterModule.forRoot([
  {
      path: '/some/route/to/standalone',
      loadComponent: () => import('./standalone.cmp').then(m => m.StandaloneRouteCmp)
  },
  {
      path: '/some/route/to/standalone/with/default/export',
      loadComponent: () => import('./default-standalone.cmp')
  }
]);

TestBed

While testing standalone components, we could avoid the need for a dedicated testing NgModule. In the simplest possible case a test could look like:

const fixture = TestBed.createStandaloneComponent(MyStandaloneComponent);

In case one needs to override components / directives in the component under test the createStandaloneComponent method could take an optional argument with overrides:

const fixture = TestBed.createStandaloneComponent(
        MyStandaloneComponent,
        {set: {imports: [ ... ]}});

Possible API choices - soliciting community feedback

While brainstorming and designing the APIs presented here there were multiple times where we had hard time deciding between multiple, equally valid options. Here we would like present alternatives considered and solicit community feedback to choose the best option.

Syntax: imports vs. other names

The current proposal uses the imports keyword to specify dependencies of a standalone component. This name was chosen for the following reasons:

  • works well with the "virtual NgModule" mental model (existing @NgModule uses imports and we plan to have the exact same semantics);
  • JavaScript uses the import keyword to denote the "bring something existing into a scope" operation and a standalone component brings an existing component, directive or pipe into its template compilation scope.

At the same time we hear the feedback where the imports word can be confused with the JavaScript imports and / or @NgModule.imports so we've also considered different names:

  • deps
  • uses

Auto-importing the CommonModule or its parts

One of the open questions is the explicitness of the dependency on the CommonModule. It should be noted that with the current proposal the CommonModule would have to be imported into the majority of non-trivial standalone components:

import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
  selector: 'with-control-statments-component',
  standalone: true,
  imports: [ CommonModule ]
  template: `
      <!-- I can use ngIf / ngFor since the CommonModule was imported -->
      <ng-template [ngIf]="show">
          <ul>
              <li *ngFor="let item of items">
                  {{item}}
              </li>
          </ul>
      </ng-template>
  `
})
export class WithControlStatmentsComponent {
    items = [...];
    @Input() show = true
}

While this is consistent with the mental model presented so far, there are alternative approaches.

The main trade off of the following variants is explicitness (at the cost of verbosity) and code succinctness (at the cost of introducing more "magic" to the system).

Our general preference is to lean towards explicitness and fine-granular dependencies with developer experience improved via tooling (compiler errors) and IDE auto-completion (via language service).

Auto-import the CommonModule

The CommonModule could be an implicit import to all standalone components. In other, words all standalone components would behave as if they always imported the CommonModule:

import { Component, Input } from '@angular/core';
@Component({
  selector: 'with-sontrol-statments-component',
  standalone: true,
  template: `
      <!-- I can use ngIf / ngFor without importing the CommonModule -->
      <ng-template [ngIf]="show">
          <ul>
              <li *ngFor="let item of items">
                  {{item}}
              </li>
          </ul>
      </ng-template>
  `
})
export class WithControlStatmentsComponent {
    items = [...];
    @Input() show = true
}

Here the component can use control flow (ngIf and ngFor) and other items from the CommonModule (like the async pipe) without an explicit import. This reduces verbosity / boilerplate code but makes Angular less explicit and "more magical".

Since we would like Angular to become more explicit and easier to reason about, this option is not preferred by the team.

Break the CommonModule into smaller modules

We could consider breaking the CommonModule into smaller ones (ex. ControlFlowModule, AsyncModule, I18nModule, ...) so users need only import parts that are actually used in a template. This would make template dependencies more fine-grained and explicit.

Additionally we could consider auto-importing a smaller module (ex. only auto-import the ControlFlowModule containing NgIf, NgFor and NgSwitch).

Mark all the directives and pipes from the CommonModule as standalone: true

To have even more fine-grained control over what is being visible to a template scope we could turn all the directives and pipes from the CommonModule into standalone ones. This would make it possible to import them individually in the @Component.imports (with the aid of IDE auto-completion powered by the Angular language service):

import { Component, Input } from '@angular/core';
import { NgIf, NgFor } from '@angular/common';
@Component({
  selector: 'with-control-statments-component',
  standalone: true,
  imports: [ NgIf, NgFor ]
  template: `
      <ng-template [ngIf]="show">
          <ul>
              <li *ngFor="let item of items">
                  {{item}}
              </li>
          </ul>
      </ng-template>
  `
})
export class WithControlStatmentsComponent {
    items = [...];
    @Input() show = true
}

Again, we could decide to auto-import certain directives / pipes.

FAQ

The future

What will happen with NgModules in the future? Will these be deprecated / removed?

The short term-answer is that NgModules are not going away and not getting deprecated - you can continue to write and consume existing NgModules.

The long-term answer is: we will monitor the adoption of standalone components, directives, and pipes, look into community feedback, and work on simplifying the overall NgModule story in a backward compatible way.

As of today NgModules have many responsibilities, some of them being very useful (ex. grouping cooperating directives), some others having alternatives (ex. the providers property vs. tree-shakable providers) and others being outright confusing. As the general direction we want to reduce or eliminate sources of complexity in the NgModule system by separating out some of its responsibilities into less tangled APIs that might eventually completely replace NgModule.

We are very much aware that NgModules play a very central role in Angular applications today, so we will move very carefully in this area.

Should I convert my apps / libraries to standalone components, directives and pipes?

We hope that the simplifications offered by this proposal will result in tangible benefits that will incentivise developer adoption. If you see benefits of using standalone components, directives and pipes — by all means please use them. If NgModule works for you - continue to use them.

Initially we are planning on providing only minimal guidance on the "standalone" vs. NgModule-based approach (mostly around library publishing). We will continue to monitor the community's feedback and update guidance as we gather more data and learnings from real-life usage.

Syntax

Do we need the standalone: true flag?

While it doesn't have to be the standalone: true flag, we need some syntax to mark components, directives and pipes as "standalone". Reasons:

  • for humans:
    • clearly communicate the intention that "you can import me without an associated NgModule and I will work correctly";
    • make it obvious what is the compilation scope of a given component (do I see all the matching components, directives, and pipes in imports or do I need to look into the associated NgModule to figure out the compilation scope?);
  • for tools:
    • speed up compilation (traverse import graph vs. scan the whole World);
    • produce a clear error message when a standalone component is misused / misconfigured; or when module-full component is imported directly (which breaks its encapsulation and results in the component behaving in unexpected/unpredictable ways).
  • for future evolution:
    • having a boolean flag could in the future (and based on the community feedback) be used to change the default from standalone: false to standalone: true. Thus enabling safe, incremental, and automatable migration.

Also check the discussion in the "Standalone components directives and pipes" section

Couldn’t we derive the standalone flag from the imports presence?

We discussed this in detail, and the consensus was that the explicitness of standalone: true is desirable for a few reasons:

  • the "standalone" concept applies to components, directives and pipes while imports only makes sense for components;
  • we can have standalone components without any imports which shows that standalone and imports are 2 different concepts;
  • on the technical side, it makes the TypeScript typings more straightforward.

Performance

Does this proposal affect the ability to tree shake components, directives, pipes and providers?

There should be no change in what Angular compiler can tree-shake.

As of today the Angular compiler needs to understand what are the components, directives and pipes used in a component's template. The generated code has only references to what is being used in a template, regardless of how many components, directives or pipes are available in a NgModule. With the introduction of the standalone components, directives and pipes the generated code won't change —imports that are not used in a template will not be part of the generated code (and thus could be tree-shaken). The only difference with standalone: true is that the compiler will have an easier time figuring out what is potentially used in a template — it can simply inspect the imports graph without going through the layer of indirection of NgModules.

Tooling

What type of tooling can we expect if this proposal is implemented?

We want to have standalone components, directives and pipes well integrated into the existing Angular tooling. More specifically:

  • the language service could help by auto-importing standalone components, directives and pipes into @Component.imports or indicate unnecessary imports based on what is being used in a template. Those are just examples but generally speaking we expect the language service to be fully integrated with the "standalone" way of doing things;
  • CLI should be able to scaffold standalone components, directives and pipes (a new option added to ng new command);
  • schematics would be used for potential future migrations (ex. converting SCAM-pattern modules into standalone: true, flipping the default value of the standalone property etc.);

Documentation and learning resources

How do we teach this?

With the introduction of the standalone components, directives and pipes Angular developers will have a choice of structuring their applications around NgModules (as of today) or around standalone components (based on the proposal in this RFC). This choice will be reflected in the different learning journeys covered in our documentation:

  • a separate learning journey for people new to Angular, wanting to start on the "standalone" path;
  • for developers familiar with Angular and NgModules we will create a "moving to standalone components, directives and pipes" learning journey - there we will be able to compare and contrast the "standalone" approach with the NgModule-centered approach;
  • a separate documentation update will be needed for library authors in order to provide precise guidance for publishing libraries compatible with both NgModule-based applications and "fully standalone" applications.

We will work on the exact documentation update plan as well as closely collaborate with Angular trainers / educators based on the RFC's feedback.

Additional resources

Read the original on github.com ↗