Skip to content

[Complete] RFC: Standalone components, directives and pipes - making Angular's NgModules optional #43784

[Complete] RFC: Standalone components, directives and pipes - making Angular's NgModules optional #43784
Oct 8, 2021 · 70 comments · 146 replies

pkozlowski-opensource
Oct 8, 2021
Collaborator

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

Replies

70 comments
·
146 replies

Thanks for the detailed explanation and examples. Looks good to me

Update (12/06/2021): Okay, after reading through comments, RFC (again), and based on my experience from other frameworks. Here are my thoughts

  1. I like the concept of Optional Modules approach at least for now and I agree this simplifies Angular mental model.
  2. And if you look at other frameworks like React, Vue, Svelte they all follow one simple architecture, component is basic building block of a web page and creating simple component is so easy and not tied any module or similar config. Angular took a first step in this direction by providing single file components (SFC). I have been using SFC in my projects and it simplifies the mental model of creating 4 files for every component. That's a good step in right direction
  3. When this feature is released I think it makes sense to default new components to Stand Alone components and make necessary changes in CLI. Eventually the modules concept can be deprecated and removed (through CLI flag). Other frameworks have shown that this model can exist and work well
  4. Based on interactions by Angular team on Twitter and Podcasts, it seems the feature is easy to implement. Perhaps an alpha version can be released with v14. I hope this feature to be well tested by GDE and community before releasing to all developers.
0 replies

This proposal looks like a great way to remove a lot of boilerplate ngModule code.

How would lazy loading / bundling work if everything was heirachially declared through component imports? Would the compiler/optimiser make good choices?

5 replies
@HymanZHAN

Yeah, I have a similar question. I remember about 2.5 years ago at Google I/O 19 (video), an example was demonstrated with pre-rendered static HTML and Angular code was loaded and injected progressively as the user interacts with the app. One key part of that demo was that code-splitting was happening at the component level to enable this progressive hydration. I haven't heard about it much since then though.

Would this optional NgModule and standalone component enable this use case in the future? Hopefully in a more or less automated manner? An initial bundle size of 10kb is really attractive.

@LanderBeeuwsaert

Same question here. I'm also wondering about the lazy loading.
We have complex UI that show +-60 different components at the same time on the screen.
Would this then be 60 different files that have to be downloaded by the browser?
Wouldn't that be a problem, in the sense that there is a max limit of concurrent files that can be downloaded? (and probably each new file to be downloaded takes some overhead to start, just guessing here)

@robwormald

there's nothing in this proposal that implies Components and files to be 1-1, as far as I can tell, nor that you'd necessarily have to use a dynamic import directly - meaning you could do more complex/intelligent things in terms of loading components as your application required:

{ 
   path: '/some/route/to/standalone', 
   loadComponent: () => someSmarterLoader('SomeComponent')

or

{ 
   path: '/some/route/to/standalone', 
   loadComponent: () => import('lazy-feature').then(m => m.ComponentA)
//lazy-feature.ts
export {ComponentA, ComponentB}
@pkozlowski-opensource

Yep, @robwormald it totally correct here - this proposal doesn't prescribe / implies single-file-components.

@pkozlowski-opensource

Re-reading questions / discussions here a couple of more clarifications: standalone components / directives and pipes don't force any particular file / packaged bundles layout. While it will be possible to have more granular bundles (one bundle per component), nothing in this RFC is prescribing / forcing it. It means that it is equally valid and possible to have one standalone component per bundle as well the entire bundle with the entire app.

In short: with this RFC we will have more possibilities but nothing prescribes component-by-component bundling and loading.

Could we have a setting in angular.json that sets/overrides the default standalone value?

2 replies
@jnizet

I'd rather not have that: reading a component source code wouldn't be enough to know if the component is standalone or not. What would be nice OTOH is to be able to specify that ng generate must generate standalone components/pipes/directives by default, without having to set the standalone option.

@IgorMinar

Thanks for the suggestion @penfold. We discussed this while creating the design and concluded that we can't have default overrides because then the components would behave differently based on this hidden (located far away) configuration.

What's worse, changing the config setting would completely change the semantics for many components (and potentially 3rd party components installed via npm). This would result in subtle runtime-only bugs that would cause your application to fall apart over time.

For these reasons, the only default is the one set in the framework, and as documented in the RFC, there is a clear path towards changing this default in a safe way if we decide that that's the right thing for the ecosystem.

Tooling section could mention possible migration schematics. Maybe language service should be leveraged during the migration to collect correct list of imports.

2 replies
@IgorMinar

Good point, @minijus. I agree. @pkozlowski-opensource could we add that?

We've discussed this, and there is a clear path forward for building schematics that will automigrate SCAM components to standalone components. In most cases it should be a safe and fully automatable migration.

For non-SCAM components the migration is a bit trickier, but as you said, the tooling could figure out the imports and do other things, and then the developer could review and approve the migration component by component. So, yes!

@pkozlowski-opensource

Mentioned potential use of schematics

  1. Awesome feature
  2. Breaking CommonModule into smaller modules would be great.
  3. I think deps or uses are better syntax than imports for dependencies in order to grasp that is a different concept
  4. At least for a while, I would like to have the standalone flag
  5. Again, awesome feature
3 replies
@IgorMinar

At least for a while, I would like to have the standalone flag

@JoelNietoTec can you please clarify what you mean by this? thank you

@BrunoAMSilva

I think he means that while ngModules aren't deprecated he would prefer to be explicit and have the flag. I would agree and that seems to be what's implied in the doc.

@alcfeoh

I'd say +1 for not using imports because I already anticipate all of my Angular learners asking: "Why does the imports in NgModule is only about modules while this one can have components and directives in it? Why aren't those in declarations instead?".
Also, a common beginner question is: "Why do we import things twice?" (referring to the Typescript import + the import in NgModule decorator).
So for the sake of illustrating that it's a different feature related to the "virtual ngModule" idea, using a different name would be very helpful.

This is a great proposal and a great step in the right direction.

  1. I'd personally +1 either:
  • Auto-import CommonModule to Standalone Components
  • Auto-import certain directives and pipes to Standalone Components namely: ngIf, ngForOf, async.
  1. The imports on a standalone component could get unwieldy.
    Consider you're using Angular Material in your component:
@Component({
  imports: [
    CommonModule,
    FormsModule,
    MatButtonModule, 
    MatIconModule,
    MatToolbarModule,
    MatMenuModule,
  ],
  standalone: true,
  selector: 'my-comp',
  templateUrl: './template.html',
  stylesUrl: ['./styles.css'],
})

Being able to import these from a constant in a separate file is good, however, it still means you're looking elsewhere for the dependencies, albeit more explicitly.

  1. Understandably, the standalone aids the development of tooling etc, but I'm not sure it should exist long term, or at least it should be v.quickly moved to be true by default.

  2. Providers can be tree-shakeable using the providedIn property.
    Could we utilise this method for Components?

If a component states that it is providedIn: 'root' then it becomes available to any Component that needs it:

@Component({
  providedIn: 'root',
  selector: 'hello-world',
  template: '<h1>Hello World</h1>'
  styles: ['h1 { color: red; }']
})
export class HelloWorldComponent { }

@Component({
  selector: 'app',
  template: '<hello-world></hello-world>'
})
export class AppComponent { }

bootstrapComponent(AppComponent);

And if you wanted to include the component in a particular NgModule, you simply replace the providedIn: 'root' with providedIn: SharedModule
There is of course the question of does the HelloWorldComponent need to be imported somewhere for the TS compilation to find it and allow it to be registered.

  1. +1 for uses rather than imports. In fact, I think that should be expanded to NgModule also. It should include another property, uses which is specific to Standalone Components. I think the concept of imports: [*Component] will be a tough mental shift for a lot of current Angular Developers. Especially if we see:
// difficult to reason about
declarations: [AComponent],
imports: [
  CommonModule, 
  FormsModule, 
  BComponent, 
  MatButtonModule
]

// easier to reason about and helps separate out that BComponent is self-contained
// whereas AComponent needs the Modules in the imports
declarations: [AComponent],
uses: [BComponent],
imports: [
  CommonModule, 
  FormsModule,
  MatButtonModule
]
  1. Overall I think this is great, and I look forward to a day where Angular's building blocks truly are single units of Components, Pipes and Directives!
0 replies

Great proposal 🚀.

+1 for Auto-import the CommonModule.

I think we can accept a little bit of magic if it means we won't have to write imports: [CommonModule, ...]
in every single component that we write.

  • Just like React removed the need to "import React" in every component;

-1 for imports: [ NgIf, NgFor ] because this will increase verbosity a lot.

Edit: How will this affect @angular/elements?

Edit2: How will this affect routing?
For ex. RouterModule.forChild(routes) and RouterModule.forRoot(routes),
What will happen with the DI part if we use RouterModule.forRoot(routes) in more than 1 standalone component?

2 replies
@mauromattos00

I agree 100%! Auto importing CommonModule would really useful!

@IgorMinar

Thanks for the feedback. The @angular/elements story will be impacted by this proposal as well, in a good way. Converting components to custom elements will become much more straightforward. We explored this a bit, but didn't have enough time to sort out all the details so we decided not to mention it in the RFC so that we are not confusing anyone.

@pkozlowski-opensource it might be worth mentioning in the RFC that @angular/elements would be positively impacted by this proposal, without going into details.

Great stuff!
In my opinion explicitely specifying imports ist better than some magic.
If the standalone flag is set upon codegeneration via the CLI it could just add the Commons Module to the imports, just as its done with ViewEncapsulation or ChangeDetectionStrategy. Specifying individual directives or pipes out of CommonModule as imports/deps/uses would be great, but it should still be possible to use the CommonModule instead for convenience.

I would stick to the name imports. deps would be the only abreviation in the decorator. uses would be fine as well, no big preference here

1 reply
@MaxKless

I agree on explicitly specifying imports.
If the angular compiler is able to tree shake the unused parts from the CommonModule anyway, it would be more convenient to import the whole thing and not worry about performance as opposed to having to import seperate ControlFlowModule, AsyncModule etc.

+1 on imports, it's instantly clear what it does and fits with the NgModule syntax.

I think this is a phenomenal proposal and am most excited for the ability to easily lazy load components at runtime and have them be split into their own bundle!

My preference with regards to the how CommonModule will relate to standalone: true would be to leave it up to the developer - though a flag in angular.json may be nice to have to determine whether it should be automatically imported or not.

In the long run, I think splitting things like NgIf, NgFor, and the AsyncPipe into standalone directives and pipes would be a welcome change.

Thanks for all of your hard work on this proposal!

1 reply
@IgorMinar

My preference with regards to the how CommonModule will relate to standalone: true would be to leave it up to the developer - though a flag in angular.json may be nice to have to determine whether it should be automatically imported or not.

Thanks for the note, Andy. I addressed this in an earlier comment. In short - this wouldn't work: #43784 (comment) - let's keep the discussion on that topic in that thread.

Great proposal! I am really looking forward to this.

Some thoughts, questions, and feedback:

  1. Some auto-importing of CommonModule provides great convenience. Like in Vue, you don't need to import anything to use v-if and v-for. If the Angular compiler is able to tree shake unused directives anyways, I'd go for auto-importing at least for a subset of CommonModule.

  2. Dividing CommonModule into smaller pieces is also great. Can be helpful to increase the explicitness.

  3. How would this affect the code-splitting strategies and lazy-loading performance? I surely believe it can affect it in a positive way 😄, but would it be possible to achieve 100% component lazy-loading in a more or less automated manner (without manually calling import everywhere)?

    As I've commented under another reply, I remember seeing the Angular team demoing this application at Google I/O 19, which was pre-rendered + progressively hydrated, with an impressive initial payload of 10kb (Now that I think more about it, it much resembles Misko's new work on Qwik). I understand that this is not being emphasized so it's probably a non-goal and I don't know how closely related these two features are, but I'd love to hear what the Angular team thinks of it as an initial payload of only 10kb is just too good to ignore. Also, various Ivy talks in various conferences mentioned its ability so I believe in Ivy's potential.

  4. How will it affect SSR/SSG?

  5. Not much preference on imports vs deps/uses, but I personally like imports better.

  6. A standalone option in angular.json would be great.

A big thank you for everyone's effort! I can't wait to see this sorted out and come to reality! 💖

3 replies
@HymanZHAN

One thing that came to mind (even though I know this is not gonna happen anytime soon), if we were to deprecate NgModule in the future, would we still have this opinionated style guide? Like if it's all just components, some architecture guidelines like "feature modules" will not exist.
I like the opinionated way Angular helps to structure applications, so much so that I am replicating it in a Vue project that I work on at work. I hope we don't lose this trait of Angular.

@LayZeeDK

In Nx workspaces, we use a workspace library as an explicit architectural boundary.

Depending on your definition, this proposal doesn't remove the need for feature modules because they often orchestrate routes and feature module injectors.

@HymanZHAN

Yeah, but NgModule is the current and recommended way to explicitly orchestrate and group things for projects that are not monorepos. If it got remove (in some distant future), wouldn't it now be the developers' burden to come up with their own solution (with the monorepo style + libs as an option)? I can somewhat foresee people trying to reinvent their own NgModules, just like how quite a few people have done in Vue and React projects. I just don't want this "opinionated" trait lost and have a dozen "community" best practices for creating your own NgModules in the future.

Thanks a lot for this great RFC!

Some remarks:

  1. standalone makes sense. The arguments why we need this are clear for me.
  2. "Magical auto imports" are OK, but then please provide an opt-out flag on component level.
    • Would it be possible to add a build time config (developer can add an array of auto imports) for the "magical auto imports" and a switch to opt-out of them on component level?
  3. How does the concrete DI tree will look like for lazy components? For lazy modules, we have an additional module level DI node as child of the root module node. Will a lazy component be added to the component DI tree like any other eager component?
  4. imports vs. deps: both have the mentioned advantages and disadvantages. deps is used for DI factory functions, with a different meaning, as well. Nevertheless, deps could be easier for beginners (because of no confusion with JS imports) and we do not necessarily need to find a matching syntax with NgModules as this concept would make them optional. I would vote for deps.
0 replies

One capability that seems to be lost with standalone components, unless I'm missing something, is encapsulation, i.e. the ability to make some components only usable within a specific feature module/component.

As far as I understand, as soon as a component is standalone, every other component/module of the application is able to import and use it, even though it has been designed to be used only as a part of another component or feature module.

This is possible, and IMHO, quite useful, with modules, which can declare components and not export them, to keep them private, and thus easily modifiable/deletable with an impact limited to their declaring module.

It would maybe be a good idea to be able to specify the authorized scope of a component, i.e. mark it as usable only in the template of some other components, or importable by only some other modules.

3 replies
@LayZeeDK

In Nx workspaces, we use a workspace library as an explicit architectural boundary. Each one having an explicitly defined public API with tooling to prevent deep or forbidden imports.

@pkozlowski-opensource

@jnizet I hear you and you are right. At the same time it moves us closer to the JS ecosystem where you've got exactly the same problem of the "package private" members. As @LayZeeDK hinted I also believe that the proper solution is to have "package" / "module" internal exports but don't expose those as part of the "public API".

@alxhub

This is indeed a downside of using standalone if you intend to carefully manage its visibility to the rest of your applications. There are two cases here:

  • A component that's used internally within a library/package, and shouldn't be visible externally.

The answer here is straightforward - hiding the component by not exporting it from the library entrypoint.

  • A component/directive that's exposed via an NgModule but shouldn't be used directly. For example, maybe the directive is an implementation detail of the system in question (such as DefaultControlValueAccessor in FormsModule).

This is a little trickier, because such components/directives still need to be exported from the entrypoint (since they will be consumed in templates, albeit indirectly). One would have to use a similar trick as Angular, to export the component with a "private" name.

Maybe it would be nice to be able to opt-in to auto-importing CommonModule. Some teams might be okay with the magic, some teams might prefer being explicit. Perhaps an application could define its own set of global uses/imports/deps? I guess that could potentially be a bit of a footgun.

2 replies
@mlc-mlapis

Let's keep it predictable and readable on the first try (without deep studying what combinations are in the play) as much as possible. It's one of the most valuable advantages of the Angular framework at all.

@MaxKless

I've seen a similar proposal in another thread but I feel like it doesn't add much in terms of readability and makes the code more confusing. If you need a set of imports that are shared between many or all components, it seems smarter to create a static array and import it as they show in the RFC.

Thoughts:

  • overall pretty awesome.
  • personally, i'd prefer a new @NgComponent decorator for this use case, vs overloading the existing @Component decorator, which would eliminate the need for standalone: true or a bunch of flag flipping. Ditto for @NgPipe/@NgDirective/@NgEtc Probably controversial, but putting it out there.
  • I'm against including the CommonModule automatically, not least of which because that introduces a dependency on @angular/common which has to be eliminated through tooling.

clearly communicate the intention that "you can import me without an associated NgModule and I will work correctly";

I wonder about this, since this assumes you have access to the source code, which is the case for code you own but not dependencies.

One thought might be mirroring the providedIn API for "standalone" components. An @NgComponent/NgDirective() could optionally add a declaredIn: SomeModule property to associate with a set of related directives / pipes / providers?

7 replies
@stefan-schweiger

I agree about the first part, but I think at least a basic set of ghings like ngIf, ngFor, etc. should at least for components always be available. It probably doesn't need to be the full CommonModule though.

@pkozlowski-opensource

@robwormald thnx for taking time to read / comment. Couple of comments:

@NgComponent / @NgPipe / @NgDirective instead of standalone: true - not too controversial - we had a similar idea and TBH I like it quite a bit. In the end decided to go with standalone: true mostly because it would be easier to "flip the default" if it comes to this (in the very distant future). But this is still on the table;

I wonder about this, since this assumes you have access to the source code, which is the case for code you own but not dependencies.

Not sure I got what you mean, we must be crossing the wires somewhere. Needs more discussion / elaboration.

@JoepKockelkorn

@pkozlowski-opensource I think what Rob means is that a library author can expose whatever he wants to expose in the docs of a library, which could mean the standalone value is not available, thus it will probably not "clearly communicate the intention" always.

@pkozlowski-opensource

Right. The assumption here is that standalone: true / false becomes part of the @Component / @Directive metadata (on the same level as a selector, a set of @Input / @Output etc.). Obviously lib authors could omit this part in their docs but this would mean that important information is not given to the users.

@wardbell

I like @NgComponent / @NgPipe / @NgDirective and not just because it eliminates the need for standalone. See why in comments I made belowL #43784 (comment)

Awesome feature, this will make applications much simpler. No more one module per component in reusable components :)
Also, I've been using the SCAM pattern without knowing its existence 😅

About the auto import of CommonModule, I'd prefer it to be explicitly set or, as other already mentioned, an opt-in config in the angular.json

0 replies

Lazy Side Effects

In order to implement feature likes ngrx's StoreModule.forFeature() and EffectsModule.forFeature(), we should be able trigger side effects that survive the declarable's lifecycle.

In the following example, (if everything is eager loaded), sayHello is added to the root injector's providers, and Hello! is displayed once even if HelloComponent is instantiated multiple times.

const SIDE_EFFECT = new InjectionToken('side effect');

@NgModule()
export class SideEffectModule {
  constructor(@Inject(SIDE_EFFECT) fns) {
    for (const fn of fns) {
      fn();
    }
  }

  static forFeature({ sideEffect }): ModuleWithProviders<SideEffectModule> {
    return {
      ngModule: SideEffectModule,
      providers: [
        {
          provide: SIDE_EFFECT,
          useValue: sideEffect,
          multi: true,
        },
      ],
    };
  }
}

@Component({
  standalone: true,
  imports: [
    SideEffectModule.forFeature({
      sideEffect: sayHello
    })
  ],
  ...
})
export class HelloComponent {}

function sayHello() {
  console.log('Hello!')
}

How can we achieve the same result without modules and without changing anything in HelloComponent's class implementation (e.g. injecting a providedIn: root service)? or maybe in other words, how could we declare providers that roll up to the root injector?

7 replies
@mikezks

Valid point that you address related to the store.
This could easily be done with a generic function that is used within the standalone Component providers. It would basically use a DI factory function an register the necessary config that is normally registered through the NgModule forFeature import.

Edit: read your commonent after I replied - so, I agree, you already described that. 😉

@pkozlowski-opensource

Yep, this is a valid use-case and I've got some ideas on how to tackle it elegantly in the DI config (roughly similar to APP_INITIALIZER but executed when a given DI context and up in a new Injector). Rough ideas for now, we are discussing it internally - stay tuned. But yeh, the general thinking is that it should be totally possible without a NgModule

@mikezks

I think it is important to have the routing and redux features available as the standalone Component feature gets available.

@pkozlowski-opensource

Yes, I agree. Not strictly part of this RFC but I believe that having ideas of the module-less APIs for those use-cases is important.

@mikezks

@pkozlowski-opensource, great to hear that - thanks a lot for your work.

DI functions are really powerful for such use cases. We can even merge different 'multi' definitions across multiple DI nodes, which is quite nice for some implementations.

Hello! First of all, this idea is amazing! Something that I want to know is if you are planning to support a "ComponentWithProviders" feature to configure standalone components.

2 replies
@rothsandro

Providers on components are already supported. Or do you mean something different?

@AsmisAlan

Sorry, I forgot to add the ModuleWithProviders link. And yes, I was talking about having something like this:

@Component({
    standalone: true,
    ///.....
})
class DateTimeFormComponent{
   static  configure(localeId: string) : ComponentWithProviders {
         return {
              component: DateTimeFormComponent,
              providers: [{provider: LOCALE_ID, useValue: localeId}]
         }
   }

    constructor(@Inject(LOCALE_ID) localeId){}
}

@Component({
    standalone: true,
    ///.....
    imports: [DateTimeFormComponent.configure('fr')]
})
class MyParentComponent {}
  1. This is a really cool feature that personally I've been waiting for years. Kudos!
  2. IMHO All the defaults suggested by the team make most sense (imports not deps; standalone flag etc).
  3. I truly hope the suggested changes are just the first step to making component, directives and pipes' dependencies more explicit and easily overridable, similar to how we handle service dependencies. Please see this feature request for a verbose explanation of what I mean and the suggested syntax: #35646
1 reply
@pkozlowski-opensource

Thnx for the reference @Maximaximum - I've linked this RFC from the mentioned issue.

This is a really cool and useful feature. Thanks and Kudos!
As someone mentioned above, a new decorator (as opposed to overloading the existing @Component decorator) is something I'd vote for.

0 replies

This is a very interesting and well-documented proposal, congrats and many thanks to the writer 👏 👏 👏

I believe it goes in the good direction, some of the most confusing parts of Angular are related to NgModule (lazy loading, transitive dependencies, routing issues, DI, etc.). It would improve developer experience, reuse and sharing specially for simple GUI componentes, and one big point to me is that it could make testing easier and with less bolierplate.

Regarding the feedback solicited:

  • I would prefer using import keyword rather than introducing a new keyword in the framework, it goes well with the existing mental model. There is some old confusion with JS imports/exports and Angular imports/exports but we have learned to live with it hehe
  • I would prefer having to import CommonModule in each standalone component rather than autoimport, it is just a bit more verbose (+1 line) but more explicit and follows the same behaviour as now. I would not split the CommonModule in parts, it would make it more confusing and less straight-forwards to use.

My two cents ✌️

6 replies
@magicmatatjahu

@bbarry The problem with lazy imports in imports is that they are asynchronous (they create Promise) and NgModules themselves do not support asynchronous ModuleWithProviders/NgModules classes and here is the problem: how and when to resolve this Promise if the DOM rendering is done synchronously?

@bbarry

It isn't without problems... the ones you describe are very likely solvable but would bring with them increased complexity and size of the framework.

This wouldn't be about actually async loading most of the time; rather the value I think would be in identifying ideal splits for webpack. If there was a way for angular identify template content required for FCP vs not, something like this could be used to give angular the ability to further optimize the initial package size.

@SanderElias

@bbarry With ESM modules, top-level await is a thing. While the async support in angular poses a lot of extra challenges, this might be an option that can be considdered. (the wait will be run before angular starts its initialization.

@pkozlowski-opensource, this might be something that needs some attention too. As I'm not 100% sure how it would pan out ;)

import {Component} from '@angular/core';

const lazyModule = await import('./template-deps')
const { FooComponent, BarDirective, BazPipe } = lazyModule

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

While the code for that seems reasonable, I'm not entirely sure this is a good idea.

@vecernik

Can foo-cmp component and bar directive be detected by compiler automatically without that boilerplate code?

@bbarry

@SanderElias I think that might almost just simply work today? Babel/Webpack will convert that into something vaguely like:

import {Component} from '@angular/core';

await import('./template-deps').then(({ FooComponent, BarDirective, BazPipe }) => {
  
  @Component({
    imports: [ FooComponent, BarDirective, BazPipe],
    template: `
      <foo-cmp></foo-cmp>
      <div bar>{{expr | baz}}</div>
    `
  })
  class ExampleStandaloneComponent {}
  

  @Module(...)
  class ExampleStandaloneHiddenModule {}

  return { ExampleStandaloneComponent, ExampleStandaloneHiddenModule };
});

the difficulty is that it is a Promise<ExampleStandaloneHiddenModule> now and so you cannot put it in the imports list without an await there (and so on all the way out to the bootstrapper). Your main.ts winds up being:

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

import { environment } from './environments/environment';

const { AppModule }  = await import('./app/app.module');

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

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

(I still don't know if this is a "good idea.")

You would need a custom webpack configuration to enable top level awaits (custom-webpack from https://github.com/just-jeb/angular-builders can probably do it).


@vecernik there needs to be some way for the angular template compiler to know which file contains the component it should load for the syntax <foo-cmp>.

While we think about naming of things and how all of this will be documented, will the group of components, directives, and pipes still be referred to as declarables? Can they still be called declarables if they may not be declared in NgModule anymore?

@Component{
  selector: ...,
  standalone: true,
  uses: [declarables],
  ...
}
export class ... 
3 replies
@pkozlowski-opensource

No, I don't think we would call all components / directives / pipes "declarables". Standalone components / directives / pipes would have to be excluded from the "declarables" set.

@samherrmann

Yeah I agree that it would be wrong to call them declarables. I'm trying to think if there's another good name for that group so that in documentation and speech we don't have to keep repeating "components, directives, and pipes". Unfortunately I don't have a good suggestion at the moment. The meaning of the name should convey "the things we use in templates".

@twopelu

Couldn't standalone components be considered as auto-declared (even they are not explicitly declared in a module), and so keep naming them as declarables? Just to keep things simple...

As someone who's been using SCAMs for some time now, standalone components make a lot of sense and will be awesome to have. As for auto-importing CommonModule, I would prefer not to because I like the explicitness of importing it when there's a need for it. I work with quite a lot of small components where CommonModule is not needed.

1 reply
@miladvafaeifard

just an idea that possible to disable (enable by default) the following example:

@Component{
  selector: ...,
  imports: [
   CommonModule.disable(),
    ngFor,
    ....
],
  standalone: true,  ...
}
export class ... 

An idea to give the option to add CommonModule somewhat "automatically" as a dependency could be to have the standalone flag support a set of constants instead of a boolean (just like it's done for change detection strategy):

@Component{
  selector: ...,
  standalone: Standalone.WithCommonModule,  ...
}
export class ... 

0 replies

I'm looking forward to seeing this proposal implemented in my projects 👍

Here's my solicited feedback:

  • use import keyword to specify dependencies
  • explicit import of CommonModule is fine. I can't foresee I'd ever have a need for the smaller CommonModule parts, but it wouldn't impact me either if those existed in addition of the CommonModule
3 replies
@alcfeoh

What if you're creating a standalone pipe or directive? Most likely it won't need ngFor, ngIf, etc. to get imported.

@PowerKiKi

If I don't need anything from CommonModule, then I wouldn't import it at all. And if I need only one thing from CommonModule then I'd import it all for the convenience of not having to remember which directives is part of which module. And all of that with only a very minimal impact on compilation time, and zero impact on runtime. That seems like an acceptable balance between convenience and performance.

@alcfeoh

You're right, I misread your comment. I thought you advocated for auto-import of CommonModule all the time, but that's not the case.

how about

@standalone()
@component({
...same thing here...
})

0 replies

I forgot to say: personally I would prefer explicit imports for CommonModule or its members. Ideally, both the individual member imports (like ngIfDirective) as well as whole CommonModule imports should be equally available.

Can't wait until the Standalone components, directives and pipes feature is released! :)

0 replies

This looks awesome. This can't happen quickly enough. Double thumbs up from me.

0 replies

Nice article. What if we forget about NgModule al together and let the compiler detect components and directives from its template and locate and use them from sources? It would greatly simplify the Angular mental model and ease the pain for newcomers.

4 replies
@LayZeeDK

It's been discussed by the Angular team to make the Angular Language Server smarter so that it suggests component imports based on its template.

@vecernik

Is Language Server also used for dev/prod builds?

@LayZeeDK

No, this would probably be something like fixers in your editor.

@Maximaximum

@vecernik It's crucial to let the developer decide which directives/components/pipes to use in each specific case. You might have multiple components that have the same selector, and it's extremely important to let YOU decide which specific directive/component/pipe to use. Therefore, intellisense should (ideally) be able to make suggestions for you, but you should declare the dependencies explicitly.

Are there any chances that this will be included into the Angular v14 release?

0 replies

tl;dr; thank you for all the feedback - our intention is to proceed with the design described in this RFC.

First of all we would like to thank everyone who commented on, or otherwise engaged in the discussion on this RFC. We had over 140 comments from more than 60 people on this RFC alone plus additional conversations through other channels (social media, meetups, conferences). We were blown away by the quality and depth of the discussion. Thank you!

Based on all the comments and feedback we believe that the design was well understood and received. Our interpretation is that the Angular community supports our intention of moving the framework in the proposed direction. And critically, we haven't seen any use-cases or technical constraints that would "break" the design.

We've solicited feedback for some specific design questions in this RFC and your input was very valuable. Incorporating this feedback in our design, we intend to:

  • require importing CommonModule, and not have it be implicitly imported in all components
  • allow individual directives/pipes within CommonModule to be imported independently
  • proceed with the imports name for the property in @Component. While the feedback shows a variety of preferences, we feel that none of the proposed alternatives capture the full meaning of this field.

In your feedback, you raised several important questions that need additional design consideration:

  • best practices for lazy-loading application parts without NgModule (with and without @angular/router);
  • bootstrapping an application without requiring an NgModule;
  • ensuring that the list of imports in @Component remains manageable, either via tooling or grouping APIs;
  • ensuring that Angular provides guidance, documentation and examples for applications opting into the "standalone" world.

Based on your feedback, we are confident enough in the core design of standalone components to proceed with implementation, but we will also embark on designing additional APIs and functionality when needed.

0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
RFCs
Labels
None yet