Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[Complete] RFC: Standalone APIs#45554

Locked
alxhub announced inRFCs
Apr 6, 2022· 36 comments· 155 replies
Discussion options

alxhub
Apr 6, 2022
Collaborator

Authors:@alxhub,@pkozlowski-opensource,@AndrewKushnir,@atscott
Area: Angular Framework
Posted: April 6, 2022
Status: Closed

A few months ago, we published an RFC for the design of standalone components, directives, and pipes. This is a project with an ambitious goal: to streamline the authoring of Angular applications by reducing the need for NgModules. The design was positively received by the community with over 140 discussion comments on the initial RFC.

This RFC complements that first proposal, and explores how standalone components will be integrated into Angular's API surface to achieve the goal of authoring applications without writing NgModules. It focuses on four areas where NgModules feature prominently today:

  • Bootstrapping an application
  • Routing and lazy loading
  • Dynamically instantiating a component
  • Providing a lifecycle hook for initialization logic

Goals

In designing these APIs, we focused on a handful of high-level goals:

  • Make it convenient to write applications without writing NgModules
  • A need to consume existing NgModules should not block use of the new APIs
  • Reduce boilerplate where possible
  • Use standalone functions instead of methods, for tree-shakability

Providers

A central role played by NgModules today is the configuration of dependency injection, and therefore of application and library functionality. Applications import NgModules, sometimes via helper functions such asStoreModule.forFeature(…), to configure behavior. Among other effects, these NgModules serve as collectors and containers for DI providers.

We believe that DI configuration can be cleanly achieved without NgModules, by working with providers and provider arrays directly. For example, today the Angular router is configured viaRouterModule.forRoot(routes, config), which accepts both the main routes for the application as well as a configuration object for the router. Both of these are then expressed in the application's DI configuration.

Instead, the router could expose separate functions for configuring router behavior, and declaring routes. This could look like:

import{configureRouter,withRoutes}from'@angular/router';providers:[configureRouter({/* router configuration */}),withRoutes([/* route declarations */]),],

As we work to reduce the complexity of NgModules in Angular, we plan to guide the ecosystem towards a providers-first approach, and away from using NgModules as configuration containers.

New APIs: Bootstrapping

The traditional flow of bootstrapping an Angular application is to declare anAppModule that names the component to bootstrap. This process involves a lot of different operations, some required and some by convention:

// First, the developer defines an AppModule which represents the application// to be bootstrapped.@NgModule({// This NgModule configures the application injector, via explicit providers// and imported dependencies.providers:[AppService,],imports:[// For example, it might configure the router for the application.RouterModule.forRoot([]),// AppModule is also conventionally responsible for configuring Angular for// the current rendering environment, by importing the correct platform// NgModule. In this case, Angular is configured for browser rendering.BrowserModule,],// AppModule specifies which component(s) should be bootstrapped onto// existing elements in the DOM.bootstrap:[RootComponent],// Conventionally, AppModule also manages the RootComponent's template by declaring it.// This also means that RootComponent's template dependencies are mixed into the// imports of the AppModule, along with application configuration.declarations:[RootComponent],})exportclassAppModule{}// Next, the developer must obtain a reference to the "platform" on the page// (a PlatformRef instance). The call also allows adding extra providers to// the "platform" level injector.constplatform=platformBrowser();// The PlatformRef is then used to bootstrap the application. Typically this step// is combined with the previous, without the intermediate `platform` variable,// but they are shown here separately to illustrate the different steps involved.platform.bootstrapModule(AppModule);// From this point, Angular waits for any application initialization Promises// (provided via the APP_INITIALIZER DI token) to resolve, and then bootstraps// the component within the NgZone associated with the application.

To support bootstrapping without an NgModule, a new functionbootstrapApplication will be introduced. Instead of configuring the application via NgModules, both the root component andproviders are specified directly:

// Platform creation is implicit, and the new API is focused around// the instantiation of an "application". The root component for this// application is named here directly, and must be standalone.bootstrapApplication(AppComponent,{// DI for the application is configured by specifying providers// directly.providers:[{provide:AuthService,useClass:JwtAuthService},,// To configure libraries which are using NgModules, the// `importProvidersFrom` function provides a bridge back to the// NgModule world.importProvidersFrom(RouterModule.forRoot([],{})),],});

TheimportProvidersFrom function serves a key role here of allowing existing NgModule-based libraries to be configured in an application bootstrapped without anAppModule. In the future, we plan to guide the ecosystem to transition to provider-based APIs for this kind of configuration.

Fallback to NgModule-based bootstrap

For more advanced use cases thanbootstrapApplication supports, we currently suggest falling back on using the NgModule-based bootstrap. This includes:

  • Multi-component bootstrap, which we intentionally chose not to support in the new API
  • Special configurations of zone.js and theNgZone
  • Using thengDoBootstrap lifecycle hook

New APIs: Router and lazy loading

The Angular Router supports lazy loading of a "part" of the application, expressed as a separate NgModule. This application part serves two distinct purposes:

  • It defines additional routes and routed components to load
  • It creates a separate injector with services scoped to those routes

We believe these are two distinct use cases, and will decouple them in the router's new standalone APIs. This leads to three new capabilities for a route definition:

  • Lazily loading an individual component
  • Lazily loading additional child routes, independently of an NgModule
  • Creating a new injector in the application hierarchy for a route and its children

Lazy loading a single component

If a component is standalone, it can be lazily loaded directly for a route, without the need to declare any extra routing configuration or NgModule to load:

exportconstROUTES:Route[]=[{path:'lazy',loadComponent:()=>import('./lazy-cmp').then(m=>m.LazyCmp)},];

To use this API, the component being loaded must be standalone.

Lazily loading additional child routes

Today,RouterModule.forChild([…]) is imported into a lazily loaded NgModule to configure child routes. When all routed components are standalone,loadChildren can be used to directly load the additional routes instead:

// app.ts:{path:'admin',loadChildren:()=>import('./admin').then(m=>m.ROUTES)}// admin.ts:exportconstROUTES:Route[]=[{path:'users',component:AdminUsersCmp},{path:'teams',component:AdminTeamsCmp},];

Separate injectors for a route

In some cases, it's desirable to scope a set of services to a route or collection of routes that represent a specific part of the application. For example, anAdminService could be scoped only to the admin routes of an application.

A newproviders option on a route allows that route to declare a set of providers which will be used to create an injector in the application injector hierarchy for that route and its children. For example, theAdminService could be provided on a route:

{path:'admin',providers:[AdminService],children:[{path:'users',component:AdminUsersCmp},{path:'teams',component:AdminTeamsCmp},],}

This would create a new injector for the'admin' route and its children, just as would happen if'admin' were configured to useloadChildren with a separateAdminModule today.

Lazily loading the injector

If the'admin' routes are lazily loaded, it may be desirable to lazily load the injector associated with them (like would happen using the NgModule API forloadChildren today). This is straightforward to accomplish through composition of the two APIs:

// app.ts:{path:'admin',loadChildren:()=>import('./admin').then(adminModule=>adminModule.ROUTES)}// admin.ts:exportconstROUTES:Route[]=[{path:'',pathMatch:'prefix',providers:[AdminService],children:[{path:'users',component:AdminUsersCmp},{path:'teams',component:AdminTeamsCmp},],];

The lazily loaded route configuration contains a single parent route which configures the injector and declares a number of child routes.

Interop with NgModules

As withbootstrapApplication, it should be possible to consume existing NgModules from the new APIs. For example, today ngrx usesStoreModule.forFeature(…) to lazily load additional functionality into the ngrx store. TheimportProvidersFrom bridge is used to do this:

exportconstROUTES:Route[]=[{path:'',pathMatch:'full',providers:[importProvidersFrom(StoreModule.forFeature()),],children:[],}];

Dynamic component instantiation

A central use case discussed in the initial standalone design is that of dynamically rendering a component. In Angular today, it's easy to write code that does so while ignoring the component's NgModule, risking problems at runtime if the component or any of its dependencies require special configuration via providers.

Setting thestandalone: true flag for a component is an indication that the component itself doesn't require configuration from an NgModule, but the same cannot be said for the component's dependencies. For that reason, a new injector is created in the application injector tree whenever a standalone component is rendered dynamically (for example, via the router), which hosts the providers from that component's NgModule dependencies. This is called astandalone injector. Consider this example:

@Component({template:`<button (click)="addDynamicCmp()">    Add a dynamic component  </button>`,})exportclassAppComponent{constructor(privatevcr:ViewContainerRef){}addDynamicCmp():void{this.vcr.createComponent(DynamicCmp);}}

The first time that the button is clicked, a new standalone injector is created (assuming one is needed), which hosts any providers from NgModule-based dependencies ofDynamicCmp. This injector then becomes the parent injector of the newDynamicCmp instance, just as ifDynamicCmp was not standalone and correctly instantiated via its NgModule.

The second time the button is clicked, the previous standalone injector can be reused. This serves several purposes:

  • it avoids the performance cost of creating a new standalone injector each time a component is dynamically instantiated
  • it ensures any services provided by the injector are singletons, allowing state to be shared across multiple instances
  • it's in line with the existing behavior of cases where Angular loads and renders components dynamically, such as via theRouter

We expect that the need for these standalone injectors will be reduced over time, as libraries switch to non-NgModule based configuration and as we move further down the path of simplifying NgModules and reducing their responsibilities.

Initialization logic

An interesting behavior of NgModules today is that they're eagerly instantiated in application injectors. That is, when bootstrapping an application:

@NgModule({imports:[FeatureModule],})exportclassAppModule{}platformBrowser().bootstrapModule(AppModule);

BothAppModule andFeatureModule will be instantiated (with constructor injection). This makes NgModule constructors behave as de facto lifecycle hooks. This effect is used in some libraries to allow for new functionality to be loaded into existing services.

The use of constructors for complex logic is not ideal (and is often considered ananti-pattern), and this pattern only works with NgModule-based configuration.

To support initialization logic without NgModules, anINJECTOR_INITIALIZER token will be supported by the application injector hierarchy.INJECTOR_INITIALIZERs are functions which are run when the injector is created, and allow for eager instantiation and other setup logic to be executed. This example injects and signals to aLoadingService that additional functionality has been loaded:

{provide:INJECTOR_INITIALIZER,multi:true,useValue:()=>inject(LoadingService).markLoaded()}

TheimportProvidersFrom function will convert the eager initialization behavior of NgModules into anINJECTOR_INITIALIZER for use in the new providers-based APIs.

Default exports

TheloadChildren and proposedloadComponent APIs accept aPromise to the type or data being loaded. They're designed to be used with a dynamicimport() operation.import() however returns aPromise to the overall ES module (file) being imported, necessitating a.then operation to pull out the desired type or value:

{path:'admin',loadChildren:()=>import('./admin.routes').then(mod=>mod.ADMIN_ROUTES),}

Alternatively, we're considering extending these router APIs to recognizedefault exports automatically. With this functionality, the above could be written as:

{path:'admin',loadChildren:()=>import('./admin.routes');}

assuming that theadmin.routes.ts file was set up accordingly:

exportdefaultconst[// admin routes];

This could reduce boilerplate when expressing lazily loaded routes, but might create confusion since the dereferencing ofdefault is done under the hood. We're interested in feedback on whether this functionality would be valuable or would be too surprising.

Specific Questions

In addition to general feedback on the proposed new APIs, we're interested in specific feedback on the following:

  1. Are there use cases in applications today where writing an NgModule is necessary that aren't served by the new APIs in this proposal?
  2. The naming ofbootstrapApplication. Should we use another verb besidesbootstrap? ShortenApplication toApp or drop it entirely?
  3. Are there additional bootstrapping configurations which you would like to seebootstrapApplication support?
  4. Is the distinction between "application" and "app component" a clear one?
  5. Should the router automatically recognize and dereferencedefault exports in its lazy loading APIs?
You must be logged in to vote

Replies: 36 comments 155 replies

Comment options

2 - IMHO NgModules are most representative of "applications" today sobootstrapApplication feels incongruous.

In order of preference:

  • render(SomeComponent)
  • bootstrap(SomeComponent)
  • bootstrapComponent(SomeComponent)

3a - I don't see an option to pass a parentinjector to any of these APIs? Plenty of use cases where I'd want to have some kind of "headless" root injector with services I'd want to share across various components. Presumably thevcr.createComponent() does this internally so it would be nice to be able to.

3b - I also don't see an ability to pass a specific DOM node as the render target, which is required for dynamic use cases (vs the "global" query-the-document for one root element style)

4 - nope, and I think it muddies the waters. I realize the teams definition of "standalone component" probably refers to "standalone from an NgModule" but imho the actual mental model is "standalone from an application"

IOW, unless you're planning to completely deprecate NgModules, it's better to stick with the concept of "NgModules-as-applications" and stick to the term "Component" (and don't use AppComponent in the examples 😉) for standalone stuff.

You must be logged in to vote
11 replies
@jadengis
Comment options

I never said they were complicated, but they certainly are cumbersome. It is a burden to have to deal with NgModules and they slow down the process of writing code. They also make it difficult to refactor code as the existing tools aren't smart enough to tell you which imported NgModules are no longer being. This makes the process of removing dependencies and trimming bundle size slower and more troublesome than comtemporaries the simply use es modules.

I have built multiple Angular apps and libraries because I like features like dependency injection, rxjs and the flexiblity that the directive / component APIs provide. My day job however is React and it is refreshing and productive to not have to deal with all these framework specific constructs. People will 100% opt for tools that speed them up and don't get in their way.

Both Vue and Svelte are quite similar in style to Angular. The performance and ergonomics of these tools is what gets people excited to use them. I just want their to be the same level of excitement for Angular.

@tayambamwanza
Comment options

I think there is still a place for NgModules, I remember that one of the problems about NgModules, is that it has too many responsibilities so possibly reducing it to one special case for advanced users might be great,

Genuine Question: What features is angular behind with due to NgModules specifically, what feature does NgModules prevent angular from having that the others do?

Anyway I feel I will only really know how good/bad initiative is when we actually experiment with it.

I guess another question I want to understand better is, is there any functionality that will be completely lost if ngModules removed?

@alxhub
Comment options

alxhubApr 12, 2022
Collaborator Author

Oh wow, I love this conversation!

I remember that one of the problems about NgModules, is that it has too many responsibilities so possibly reducing it to one special case for advanced users might be great

Specifically, I think one of the biggest complexities with the design of NgModules is that they have multiple responsibilitieswith different scopes. NgModules are how we configure component templates, and in that sense they are extremely hierarchical with imports/exports defining "public" and "private" effects within a given NgModule. However, they also configure dependency injection, which is a global operation - providers in any NgModule in the application are visible toall components in the application - the same concepts of "public" and "private" don't apply (caveat: at least before lazy loaded injectors are considered).

This dual nature makes them extremely interesting as a solution, because they enable components to "just work". If you import the NgModule for a component into your application, you can use that component in your template, and any services required by the componentor its dependencies are transitively imported into your application. You don't have to think about it!

Except, actually, you do:

Genuine Question: What features is angular behind with due to NgModules specifically, what feature does NgModules prevent angular from having that the others do?

This dual nature of NgModules leaks through the APIs in a few different ways. When you set up lazy loading in your application, you quickly discover that you can't just lazily load a component. You need to load its NgModule - components can'tstand alone in Angular (yet!). If you're using the router for lazy loading a bunch of components at once, this is only mildly inconvenient. If you're trying to lazy load specific components in routes individually, it gets tedious. And if you're lazy loading without the router, you have even more responsibility: it'syour job to ensure the NgModule side of a component is accounted for when instantiating it. You need to load the NgModule, create an injector with the right parent, andthen you can create the component on top of this injector.

So to answer@tayambamwanza's excellent question, lazy loading a component individually is one of those things we can't do today, because Angular's current components aren't guaranteed to work without their NgModule in the picture. There's no way we could offer theloadComponent API from this RFC without something like standalone.

NgModules aren't a terrible API: they work, and they provide real value in large applications. But that doesn't mean we can't do better :)

@jadengis
Comment options

NgModules aren't a terrible API:

I agree they aren't terrible. I've always been okay with NgModules as a tool for configuring the injector because its very reminiscent of how other DI solutions setup their providers (spring / dagger / guice). My first experience with Angular however was actually AngularDart which has no concept of NgModules and uses adeps array in the component metadata for specifying the compiler scope (similar to the proposal for standalone components in Angular). As a result, I've never liked how NgModules were used to configuration compiler scope and have always found it cumbersome.

In removing the requirement for using NgModules for compiler scope, I think it's natural to ask if we need NgModules at all. Is all that conceptual overhead worth it to just configure the injector? I have a hard time thinking of a case where an NgModule is better than a simple array of providers. Lazy loading of providers is all ready supported out of the box if we simply provide on a routed component. If you peel back the layers I think the value NgModules provides begins to erode.

I for one am bullish about a future where they are no longer needed.

@tayambamwanza
Comment options

@alxhub Thanks for explaining, never thought of that actually.

Comment options

This was a great RFC 🚀

Questions:

  • How does the new standalone apis play withngExpressEngine (Universal)? As far as I know, it accepts a bootstrap module (ServerAppModule). Anything about that?
  • Why can't we have the zone options (ngZoneEventCoalescing, ngZoneRunCoalescing) inbootstrapApplication? Are they going away?

Answers:

  • bootstrap function feels just right.
  • Router should automatically recognize and dereference default in order to reduce some code.
You must be logged in to vote
1 reply
@alxhub
Comment options

alxhubApr 7, 2022
Collaborator Author

How does the new standalone apis play with ngExpressEngine (Universal)? As far as I know, it accepts a bootstrap module (ServerAppModule). Anything about that?

Yes - at least as far as the base@angular/platform-server APIs, we will support a similar standalone version ofrenderModule, that will mirror the design ofbootstrapApplication.

Why can't we have the zone options (ngZoneEventCoalescing, ngZoneRunCoalescing) in bootstrapApplication? Are they going away?

No, they're not going away. We're trying to keep the new bootstrapping API as minimal as possible for now, and consider cases likeNgZone configuration as we expand its capabilities. It may be that having these options as first class options in the bootstrap API is not the right design. We want to get the core functionality right first, and then explore these questions.

Comment options

Component-level initializer

How about an initializer tied to the injector scope of a standalone component assuggested by Younes (@yjaaidi)?

@Component({standalone:true,providers:[{provide:INJECTOR_INITIALIZER,multi:true,useValue:()=>inject(LoadingService).markLoaded()}],template:'',})exportclassMyStandaloneComponent{}
You must be logged in to vote
2 replies
@alxhub
Comment options

alxhubApr 8, 2022
Collaborator Author

This is an interesting idea and something we've discussed before. Currently the thinking is not to do this, for a few reasons:

  1. It's already possible to react to component instantiation, via thengOnInit API.
  2. There would be some overhead to check forINJECTOR_INITIALIZER on every component instantiation.

But I do see the potential for better modularity - this is something we should think about.

@yjaaidi
Comment options

Thx Lars for reraising this issue 😊

Comment options

Route support for default exports

Yes, please. In addition to a named export similar to the current Router API.

// app.component.tsimport{Component}from'@angular/core';import{configureRouter,RouterModule,Routes,withRoutes}from'@angular/router';constroutes:Routes=[{path:'admin',loadChildren:()=>import('./admin.routes'),// 👈},];@Component({standalone:true,imports:[RouterModule,],providers:[configureRouter({anchorScrolling:'enabled',initialNavigation:'enabledNonBlocking',scrollPositionRestoration:'enabled',}),withRoutes(routes),],template:'<router-outlet></router-outlet>'})exportclassAppComponent{}
// admin.routes.ts//       👇exportdefaultconstroutes:Routes=[// or:// export default const [{path:'users',component:AdminUsersComponent},{path:'teams',component:AdminTeamsComponent},];

versus

// app.component.tsimport{Component}from'@angular/core';import{configureRouter,RouterModule,Routes,withRoutes}from'@angular/router';constroutes:Routes=[{path:'admin',loadChildren:()=>import('./admin.routes').then(esModule=>esModule.routes),// 👈},];@Component({standalone:true,imports:[RouterModule,],providers:[configureRouter({anchorScrolling:'enabled',initialNavigation:'enabledNonBlocking',scrollPositionRestoration:'enabled',}),withRoutes(routes),],template:'<router-outlet></router-outlet>'})exportclassAppComponent{}
// admin.routes.tsexportconstroutes:Routes=[// 👈{path:'users',component:AdminUsersComponent},{path:'teams',component:AdminTeamsComponent},];
You must be logged in to vote
2 replies
@mikezks
Comment options

Definitely a good concept that may be used in the majority of lazy use-cases and reduces complexity for beginners. Nevertheless, the named exports are important to implement more advanced concepts.

Comment options

Follow ups:

  • as of now, all of the styling APIs (including the defaultViewEncapsulation.Emulated but also the significantly-more-useful-with-standalone-componentsShadowDom) live in the platform-x packages, thus while your samples are technically standalone they're going to require importing/providing theBrowserModule, no?
  • is the intention to port the emulation / shadow root functionality to core?
  • regarding Zones, this RFC doesn't appear to expose a public API for triggering change detection, which again is critical for many more flexible use cases. I'd argue that Zones should be opt-in for standalone components but that's probably out of scope.

To illustrate the basic idea:

@Component({template:'Hello {{name}}!'})exportclassHelloWorld{  @Input()name='World'}consthost=document.getElementById('root');constcompRef=bootstrapComponent(HelloWorld,{host});//a few moments latercompRef.instance.name='Angular';//compRef.detectChanges() ?//detectChanges(compRef) ?
You must be logged in to vote
1 reply
@pkozlowski-opensource
Comment options

Very interesting questions here and parts we didn't call out in the RFC explicitly (partly to avoid "details overload" and partly because we are still debating things). But here we go:

as of now, all of the styling APIs (including the default ViewEncapsulation.Emulated but also the significantly-more-useful-with-standalone-components ShadowDom) live in the platform-x packages, thus while your samples are technically standalone they're going to require importing/providing the BrowserModule, no?

Yes. If we importbootstrapApplication from platform browser then the relevant providers should be included. Also see the discussion in another question:#45554 (comment)

is the intention to port the emulation / shadow root functionality to core?

This is not part of the "standalone" effort.

regarding Zones, this RFC doesn't appear to expose a public API for triggering change detection

ApplicationRef is still this API:

constappRef=awaitbootstrapApplication(HelloWorld);//a few moments laterappRef.components[0].instance.name='Angular';appRef.tick();

But yeh, this RFC centers the idea of "bootstrap" around the "application" concept vs. "root component" concept.

Comment options

How does the new standalone apis play with ngExpressEngine ?

You must be logged in to vote
1 reply
@pkozlowski-opensource
Comment options

This was answered in another question, see:#45554 (reply in thread)

Comment options

Lazy loading components requires a "loader module" next to the component.
Is this also the case for standalone components? In general loader modules are really clutter for the codebase so I am curious on plans in that direction.

You must be logged in to vote
2 replies
@LayZeeDK
Comment options

This proposal suggests that no Angular modules are needed to lazy load a routed standalone component:

exportconstroutes:Routes=[{path:'lazy',loadComponent:()=>import('./lazy-standalone.component').then(esModule=>esModule.LazyStandaloneComponent)},];

The same goes for dynamically loaded standalone components:

@Component({template:`<button (click)="addDynamicComponent()">    Add a dynamic component  </button>`,})exportclassAppComponent{constructor(privatevcr:ViewContainerRef){}addDynamicComponent():void{this.vcr.createComponent(DynamicStandaloneComponent);}}

I think you haverender modules in mind. Hopefully, they are not needed because a standalone component has something like a localtransitive compilation scope/local component scope.

@alxhub
Comment options

alxhubApr 7, 2022
Collaborator Author

@LayZeeDK is correct - no NgModules are necessary to lazily load standalone components. That means no loading NgModules, and no render NgModules.

Comment options

I'm curious too., have a good work.@BioPhoton

You must be logged in to vote
0 replies
Comment options

TestBed API

Please consider feedback on the extendedTestBed API for Standalone Components.

CommonModule default import

CommonModule is currently implicitly imported by the Angular module configured byTestBed.configureTestingModule.

Given this component

// my-standalone.component.ts@Component({standalone:true,template:'{{ text$ | async }}',})exportclassMyStandaloneComponent{}

This component would fail at compile time (AOT) or runtime (JIT) becauseCommonModule was left out of itsimports metadata even though it might not in its component test suite becauseCommonModule is implicitly imported. Personally, I don't think that standalone components should implicitly importCommonModule. I don't want to start up that particular discussion again but there could be a difference between compile time/runtime and component tests because of this.

Your suggestion in the Standalone Components RFC

constfixture=TestBed.createStandaloneComponent(MyStandaloneComponent);

or with metadata overrides that must supportimports andschemas, maybe evenstandalone?

constfixture=TestBed.createStandaloneComponent(MyStandaloneComponent,{set:{imports:[ChildComponentStub,CommonModuleStub,],},});

Younes' suggestion in the Standalone Components RFC

Suggestion by Younes (@yjaaidi):

constfixture=TestBed.createComponent(MyStandaloneComponent);

My suggestions in the Standalone Components RFC

Suggestions by me
Standalone component with metadata overrides

TestBed.overrideComponent(MyStandaloneComponent,{set:{imports:[ChildComponentStub,CommonModuleStub,],},});constfixture=TestBed.createComponent(MyStandaloneComponent);

Legacy component with stubbed provider

TestBed.configureTestingModule({declarations:[UserComponent],providers:[{provide:UserService,useClass:UserServiceStub}],});constfixture=TestBed.createComponent(UserComponent);

Standalone component with provider stubbed in testing module

TestBed.configureTestingModule({providers:[{provide:UserService,useClass:UserServiceStub}],});constfixture=TestBed.createComponent(UserComponent);

Standalone component with provider stubbed in metadata overrides

TestBed.overrideComponent(MyStandaloneComponent,{add:{providers:[{provide:UserService,useClass:UserServiceStub}]},});constfixture=TestBed.createComponent(UserComponent);

Legacy component with inline template and styles

TestBed.configureTestingModule({declarations:[UserComponent],});constfixture=TestBed.createComponent(UserComponent);

Standalone component with inline template and styles

constfixture=TestBed.createComponent(UserComponent);

Legacy component with external template and styles

TestBed.configureTestingModule({declarations:[UserComponent],});constfixture=TestBed.createComponent(UserComponent);

Standalone component with external template and styles

constfixture=TestBed.createComponent(UserComponent);

Alternative suggestion by me in the Standalone Components RFC

Suggestion by me

TestBed.configureTestingModule({imports:[UserComponent],providers:[{provide:UserService,useClass:UserServiceStub}],});constfixture=TestBed.createComponent(UserComponent);
You must be logged in to vote
3 replies
@pkozlowski-opensource
Comment options

Good discussion on the TestBed + standalone story! We didn't cover theTestBed APIs since we didn't spend enough time to dig into all the details. We want to spend a bit more time in this area to explore and see what needs to change.

@krilllind
Comment options

We heavily provide services in the@Component() decorator to make sure we clean up resources once destroyed. However, while unit-testing a component you always have to call.overrideComponent(AppComponent, { set: { providers: [] } }); for services in.configureTestingModule() to be used.

With the introduction of standalone components, I believe that TestBed API need's some improvement to help with mocking. I could see an API something like this being useful:

describe("AppComponent",()=>{letfixture:ComponenetFixture<AppComponent>;letcomponent:AppComponent;beforeEach(async()=>{letauthServiceStub=jasmine.createSpyObj("AuthService",["login"]);awaitTestBed.compileComponent(AppComponent,{providers:[{provide:AuthService,useValue:authServiceStub}]});fixture=TestBed.createComponent(AppComponent);component=fixture.componentInstance;});});
@yjaaidi
Comment options

We can use theTestBed.configureTestingModule for standalones too.
Turns out quite useful for shallow testing.

TestBed.configureTestingModule({declarations:[MyCmp],providers: ...,schemas:[CUSTOM_ELEMENTS_SCHEMA]})TestBed.createComponent(MyCmp);

The trick here is to make sure thatMyCmp's imported providers are ignored if already defined inTestBed.configureTestingModule.

Comment options

Will NgZone be supported?

WillbootstrapComponent(MyStandaloneComponent) supportNgZone at all?

You must be logged in to vote
3 replies
@nisancigokmen
Comment options

most likely,yes :)

@alxhub
Comment options

alxhubApr 7, 2022
Collaborator Author

It will only support zone-based applications. Unlike thebootstrapModule() API, there is no option to disable zones (at least initially - we will explore zone configuration in the future, as mentioned above).

@LayZeeDK
Comment options

Thank you for the clarification, Alex♥️

Comment options

bootstrapComponent options

A. WillbootstrapComponent support passing a parent injector?
B. WillbootstrapComponent support specifying a host element?

You must be logged in to vote
12 replies
@mikezks
Comment options

Would it be possible to callbootstrapApplication() more than once and the platform gets reused automatically? Or will that throw an error?

@alxhub
Comment options

alxhubApr 8, 2022
Collaborator Author

Would it be possible to call bootstrapApplication() more than once and the platform gets reused automatically? Or will that throw an error?

Yes, this should work just fine. That's the distinction between platform and application - the page only has one platform, but there can be many applications running on it simultaneously.

@mikezks
Comment options

@alxhub
That sounds interesting. If an already created platform is shared automatically, then this would even be a better situation for Micro Frontends as we have now.

To sum it up,bootstrapApplication() does several things behind the covers:

  • It instantiates a singleton platform w/i the same technical context (bundle or Module Federation shared dependency) and does not error on a second attempt, but uses the already created one (today it errors).
  • It instantiates a new application per call w/i the singleton platform.
  • It instantiates the referenced Component.

Is this correct?

// CC@manfredsteyer

@wilmarques
Comment options

@mikezks I guess if you reuse the injector, the providers will be shared.

@mikezks
Comment options

Thanks@wilmarques.

There is one special edge case for multi version Micro Frontends with Module Federation, where we need to reuse the platform, but bootstrap a second independent app.

It seems, that this can work with the new API like today or even a bit easier.

Comment options

Which platform does bootstrapComponent use?

A. Which package will hostbootstrapComponent?@angular/core or@angular/platform-browser and friends?
B. WillbootstrapComponent provideDomSanitizer?

You must be logged in to vote
2 replies
@alxhub
Comment options

alxhubApr 7, 2022
Collaborator Author

Which package will hostbootstrapComponent?@angular/core or@angular/platform-browser and friends?

This is not yet decided. I think there is a lot of interest on the team in putting the API in@angular/core, but that may require significant refactoring / reorganization of code, and we may choose to delay this work until a future version.

WillbootstrapComponent provideDomSanitizer?

Yes - it will implicitly bring inBrowserModule and thus provide all the same services as an NgModule-based bootstrap.

@pkozlowski-opensource
Comment options

Just to update everyone on our latest thinking:bootstrapApplication will be exposed from the@angular/platform-browser: so,import {bootstrapApplication} from "@angular/platform-browser". As part of this bootstrap call all the browser-related providers from theBrowserModule (ex.:DomSanitizer) will be included.

For the SSR scenarios we are going to expose therenderApplication function from@angular/platform-server.

Comment options

Great RFC!

Answers to Questions:

2. The naming ofbootstrapApplication. Should we use another verb besidesbootstrap? ShortenApplication toApp or drop it entirely?

Naming:bootstrapComponent. Changing to:bootstrapApplication. See reasoninghere

5. Should the router automatically recognize and dereferencedefault exports in its lazy loading APIs?

No. Please don't encourage default exports. They are no good. I don't mind a bit extra boilerplate for the sake of expressiveness and other benefits. More info on the subject:

You must be logged in to vote
6 replies
@LayZeeDK
Comment options

How do you feel about the existing name of PlatformRef#bootstrapModule,@alxhub? It doesn't mentionapp orapplication.

Say in a future version, bootstrapComponent would support NoopNgZone, would bootstrapApplication still be your preferred name?

To me, "bootstrapping" in Angular means start an "application" whether by Angular module or component. Attaching a component with a life cycle to the DOM would be a different thing in which case bootstrapComponent wouldn't be a descriptive name.

The reason I prefer bootstrapComponent is the distinction from bootstrapModule, that only a component is required. "bootstrap" is a decent name but would types be able to distinguish between passing a component and passing any other class/decorated class?

@pkozlowski-opensource
Comment options

How do you feel about the existing name of PlatformRef#bootstrapModule

With the general direction of makingNgModule concepts / APIs "less prominent" I think that leavingbootstrapModule goes against this goal.

"bootstrap" is a decent name but would types be able to distinguish between passing a component and passing any other class/decorated class?

Yeh, "bootstrap" is generic and "vague enough" that we side-step the whole detailed discussion of "application" vs. "root component". As you've mentioned it is a decent name and an option that is still on the table.

@LayZeeDK
Comment options

Would the bootstrap function's types be able to tell whether we pass a (standalone) component, an Angular module, or some other class by mistake?

@alxhub
Comment options

alxhubApr 8, 2022
Collaborator Author

@LayZeeDK at the type level, no. This is not possible in TypeScript, since there is currently no way for decorators to alter the type they're decorating. Perhaps this will change as decorators are formally added to JavaScript itself.

@LayZeeDK
Comment options

In that case, should we rely on the function name, that is bootstrapComponent, and/or the parameter name, that is bootstrapApplication(component)?

Comment options

What about Angular Elements? Currently it's needed to create a module and bootstrap the main component onngDoBootstrap.

But it isn't a great opportunity to also simplify that process?

You must be logged in to vote
2 replies
@alxhub
Comment options

alxhubApr 7, 2022
Collaborator Author

Yes, and we are very much interested in ways we can improve Angular Elements. A lot of these improvements, though, go beyond the specific use of NgModules, and would depend on deeper semantic changes in how elements work (for example, how elements share their "application" / zones). This is better explored as a separate effort.

@wilmarques
Comment options

@alxhub , that makes sense. Can't wait for that!

But what if we start experimenting with something similar to what was proposed for bootstrapping applications?

Maybe something like this:

// We could use `bootstrapElement` instead of `bootstrapApplication`bootstrapElement(AppComponent,{providers:[    ...importProvidersFrom(RouterModule.forRoot([],{})),],});
Comment options

Are there use cases in applications today where writing an NgModule is necessary that aren't served by the new APIs in this proposal?

I don't know of any.

The naming of bootstrapApplication. Should we use another verb besides bootstrap? Shorten Application to App or drop it entirely?

I Agree with@robwormald here.

  1. render(component, {OptionalInjector, optinalRootZone})
  2. bootstrap(component, {OptionalInjector, optinalRootZone})

However, I disagree with Rob about the DOM node, which should be taken care of by the selector in the component. I don't see a use-case where one would need to boot the same 'top' component multiple times in a document. I don't see this as an extension/replacement for Angular Elements. (although that might be worth a discussion on its own!)

Are there additional bootstrapping configurations which you would like to see bootstrapApplication support?

Yes, proper support for booting multiple components, using the same Injector. This will bring much of the value from micro frontends without most of their usual costs.
But also enable a way to use different Angular versions (forward-facing) to bootstrap components on the same page. While those will not be able(or should not) to share an Injector, it also makes the use of micro frontends easier. There should be enough warnings in the docs/developer console to makeclear that this isn't the most optimal way of doing things.

Is the distinction between "application" and "app component" a clear one?

Well, without a ngModule, the app component becomes the app, right?
I think that will be the common "trap" for most devs.
While I do get that the app holds the component, the component isnot the app, it might need some additional documentation and reasoning.

  • Is it technically really needed for a dev to know?
  • if yes for the above, it needs proper explanation. (also, in this caserender makes even more sense asbootstrap for that question)
  • What are the pitfalls if someone doesn't realize this?

Should the router automatically recognize and dereference default exports in its lazy loading APIs?

Yes, please!

You must be logged in to vote
5 replies
@alxhub
Comment options

alxhubApr 7, 2022
Collaborator Author

Yes, proper support for booting multiple components, using the same Injector. This will bring much of the value from micro frontends without most of their usual costs.

I haven't worked much with micro frontends personally - can you elaborate on the use case here? Do microfrontends often bootstrap multiple components at a time? Is this not possible today using theApplicationRef.bootstrap() API (dynamic bootstrap) to bootstrap additional components within the application?

@SanderElias
Comment options

The use case is having a mono-repo with multiple teams working on different "mini-apps" that can be active on the same page, at the same time. The page can be a SPA on its own or an old-fashioned MPA. The teams can work mostly in isolation from each other.
Also, the same setup, but then with each team their own repo, and a more sophisticated deploy pipeline.
This is a business solution more than a technical solution and comes with the cost of a heavier payload for the page. The perceived value is that each team can have its own velocity/freedom.

Personally, I have only tried it in lab settings and kicked it around a little bit, and its a while ago. Yes it Is possible with current tools, but there are some issues IIRC. I think@manfredsteyer can provide way more details around this as I can.

@mikezks
Comment options

I will add a comment covering all possible Micro Frontend implications.

One scenario that would likely break is:

  • Different builds with Module Federation
  • Same Angular version setup during runtime
  • At least two bootstrapped Standalone Components, which can not be used w/i the same Platform instance
  • Today, Angular throws an error, if we instantiate more than one Platform w/i the same versionand shared Angular dependency

// CC@manfredsteyer

@manfredsteyer
Comment options

Thanks for CC-ing me,@SanderElias and@mikezks. You are right: In order to support all the different "flavors" of micro frontends we find in huge companies, it would be great to a) be capable of switching out ngZone and b) bootstrap several applications using the same platform (platformBrowser).

However, I understand that not all of these things can be directly supported at the beginning.@alxhub already mentioned, that there will be another initiative for dealing with Zone.js. Anyway, I think we will find ways for doing micro frontends, e. g. by sticking at least with the traditional bootstrapping and at least with one AppModule.

To put it short: If a) and b) do not hurt, e. g. by providing a parameter object with a ngZone property, it will help micro frontend folks a lot. Otherwise, they can also live with it (but won't be that happy, also b/c as all of this is straightforward in other frameworks).

@wilmarques
Comment options

I'm not sure if I would use Angular for the use case I'm going to describe.

But imagine a Design System library created using Angular Elements. If it was possible to bootstrap several components within the same project it would be possible to export each of these components as Angular Elements.

Although it's possible using the currently available options, I agree with@manfredsteyer that it isn't so straightforward as LitElement, for example.

Comment options

Just more conventions over configurations...

I'll try to go to the extreme, just pushing boundaries a little bit :-).

  • bootstrap(rootComponent: AppComponent, providers: Provider[]): Promise<void>
    • I prefer a one-word name, a few times mentioned in the comments, I like the name for its simplicity (...and it can be interpreted as bootstrapping an application). I think it can work for Angular. Libraries like Solid, and React are usingrender verb, sobootstrap verb can be an option for Angular.
    • No config object, only array of providers. I see providers as kinda config, but is it doable in this case?
    • Return value isPromise.

2a)

// just boostrap a simple app without providers.bootstrap(AppComponent);

2b)

// a real app with configuration defined using InjectionTokens,...awaitbootstrap(AppComponent,[// { provide: BOOTSTRAP_OPTIONS, useValue: {...}}useBootstrapOptions({ngZone:'zone.js',ngZoneEventCoalescing:true,}),// { provide: ROUTER_OPTIONS, useValue: {...}}useRouterOptions({initialNavigation:'enabled',}),// { provide: ROUTES, useValue: [] }useRoutes([]),// ...{provide:ErrorHandler,useClass:MyErrorHandler},{provide:LOCALE_ID,useValue:'cs',},]);console.log('App is running');

And thank you for such a great RFC.

You must be logged in to vote
2 replies
@alxhub
Comment options

alxhubApr 8, 2022
Collaborator Author

No config object, only array of providers.

We've learned an important lesson in years of designing APIs in Angular: that just because today we can't imagine more parameters or options to a function, doesn't mean there won't ever be new things to add to an API. So now we try to future-proof these kinds of APIs using an options parameter that can be expanded as needed with new keys.

@otodockal
Comment options

I see. Thank you for the answer, Alex.

Comment options

Hi, this RFC is amazing! congrats!

I want to know is if you are planning to support a "ComponentWithProviders" feature to configure standalone components, similar to the ModuleWithProviders for modules.

@Component({///.....})classDateTimeFormComponent{staticconfigure(localeId:string) :ComponentWithProviders{return{component:DateTimeFormComponent,providers:[{provider:LOCALE_ID,useValue:localeId}]}}constructor(@Inject(LOCALE_ID)localeId){}}@Component({standalone:true,///.....imports:[DateTimeFormComponent.configure('fr')]})classMyParentComponent{}

And may will be useful for routing too.

@Component({///.....})classChildComponent{staticconfigure(data) :ComponentWithProviders{return{component:DateTimeFormComponent,providers:[{provider:DATA_INJECTOR,useValue:data}]}}constructor(@Inject(DATA_INJECTOR)data){}}// router{path:'child-component',loadChildren:()=>import('./child-component').then(component=>component.configure({config:"123"})),}
You must be logged in to vote
1 reply
@pkozlowski-opensource
Comment options

No, this is not our intention. Mainly we don't want to turn components into a component + NgModule combination (in the sense of being an union of all capabilities of a componentand NgModule).

Comment options

Is it possible that Standalone Components are released before these APIs?

Will you wait on figuring out these APIs before releasing the Standalone Components feature itself? Standalone Components will be useful even before the release of these APIs.

You must be logged in to vote
3 replies
@pkozlowski-opensource
Comment options

Technically nothing prevents us from doing so. And this is the order in which we implement things: standalone components, directives and pipes, interoperability with NgModules, injector story etc. Then moving to the APIs (that need the pieces mentioned before).

@tayambamwanza
Comment options

Yeah this would be great, I think some more new ideas will arise just from using the basic concept of standalone components for now so look forward to when they release.

@jadengis
Comment options

I'd love to get standalone components early. I have been using the single component module pattern for my shared components for a while now and it would be great to clean up that mess.

Comment options

+1 for the default exports and separate injectors for a route.

You must be logged in to vote
0 replies
Comment options

Bootstrapping

The new suggestedbootstrapApplication function only supports one component, where the previousbootstrap property in ngModule accepted an array. I believe this new bootstrapping logic should be able to handle both. I also think the namebrowser could be included to clarify intent between browser and server, allowing for existing moduleBrowserModule to be included by default. I would suggest something like the following:

bootstrapBrowserApp({declarations:[RootComponent],providers:[{provide:AuthService,useClass:JwtAuthService},importProvidersFrom(RouterModule.forRoot([],{}))]}):

Currentmain.ts file also configuresenableProdMode() under an if-statement, I would really like to see this included in the bootstrapping configuration, perhaps as just another property likeproductionMode: environment.production

With the change I now assume that in this caseRootComponent will declare all other component which are used within it's template?

Routing

In the case of route configuration extracted into separate files, I believe that adding aproviders property gives the same kind of disconnect we have today with ngModules where available injectables are not clear from a component standpoint. I was hoping we could consolidate into just theproviders property of@Component() configuration. However, we need a distinction between "route injection provider" and "component injection provider" as we want some injectables to be destroyed when component is destroyed.

Could developers maybe import functions used inproviders property to clarify scope?

import{AService,BService}from"...";import{transient,scoped}from"@angular/core";@Component({selector:'lazy',providers:[transient(AService),// <- Will be destroyed when component is destroyedscoped(BService)// <- Will remain in route provider tree and reused when component is created again]})classLazyComponent{constructor(privatea:AService,privateb:BService){}}

This concept borrows a bit from .NET DI where you have.AddTransient() and.AddScoped() methods.

Default export

I initially was very pro this idea, but after thinking for a while I do believe this "magic" doesn't really provide much value. The difference between

{path:'admin',loadChildren:()=>import('./admin.routes').then(chunk=>chunk.ADMIN_ROUTES),}// and{path:'admin',loadChildren:()=>import('./admin.routes');}

is so few extra characters of code, IMO being explicit in this case should not be a big deal, even in enterprise applications with hundreds of routes. This will probably help with debugging as well where you actually can provide the name of the exported variable in a stacktrace.

You must be logged in to vote
12 replies
@LayZeeDK
Comment options

@alxhub
I haven't specified multiple components to be bootstrapped. I imagine using it for microfrontends.

What would be the impact of multiple bootstrapped components declared in a single root Angular module? What's their application model?

@krilllind
Comment options

@LayZeeDK Ah I missed that API specification! You are right, with introduction ofimports property I definitely think the newbootstrapApplication function should also make use of the same terminology and not usedeclarations!

@krilllind
Comment options

@alxhub
I've bootstrapped two components before so that I can make use of both of them inindex.html.
RootComponent acts as the root of my application while my second component,LoadingComponent displays an initial splash screen on top of my application while establishing connection to backend (and verifying some configuration).

If I provide a service asroot or inAppModule.providers scope of my application, I get the same instance of this service in bothRootComponenet andLoadingComponent, so think makes me think that we are not currently creating two different provider scopes.

I could also, with standalone components, see the use case of bootstrapping a modal container in which you could through a service provide an API to dynamically show content without polluting theRootComponent.

@LayZeeDK
Comment options

@krilllind
You can use the platform provider scope ({ providedIn: 'platform' }) to share dependencies across Angularapplications.

@jnizet
Comment options

@alxhub Cédric Exbrayat and I use multiple bootstrapped components in the HTML slides of our Angular training. the slides are a regular index.html file, containing slides written with bespoke.js. And some of the slides contain a live Angular demo, implemented as a bootstrapped component.
We did it that way because it seems to be the obvious way to do it.

Comment options

Impact of optional-NgModules on the next milestones in Angular roadmap

I'd be happy to understand whether introducing the optional-NgModules cantechnically help Angular to move forward with the next milestones from the Angular roadmap, including the latest innovations in the web industry.

Thank you for your hard work for constantly improving Angular! I'd like to share my impression (maybe wrong?) that Angular compiler is very sophisticated, which (I guess?) makes the adoption of the latest innovations from web industry more tricky. The support of dynamic-imports for example was added quite lately (comparing to Vue and React) when ViewEngine was rewritten to Ivy. And I'm wondering if dropping NgModules will significantly help to simplify the Angular compiler, so Angular can adopt new innovations in the web industry more eaisly. But again, maybe it's just my wrong impression. You folks are doing great job developing Angular, many thanks!

TheAngular roadmap touches - among others - the following 3 topics:

Investigate modern bundles
To improve development experience by speeding up build times research modern bundles. As part of the project experiment withesbuild and other open source solutions, compare them with the state of the art tooling in Angular CLI, and report the findings.

Improved build performance with ngc as a tsc plugin distribution
Distributing the Angular compiler as a plugin of the TypeScript compiler will substantially improve developers' build performance and reduce maintenance costs.

Explore hydration and server-side rendering usability improvements
As part of this effort we'll explore the problem space of hydration with server-side rendering, different approaches, and opportunities for Angular. As outcome of this project we'll have validation of the effort as well as a plan for action.

Moreover, on the horizon we can see the birth of the next generation of "resumable-SSR" freameworks (Marko and Qwik). There aresome pending works to integrate Qwik with React. It's interesting when Angular will be able to adopt similar innovations.

Do you think that any of above topics will be simpler to achieve for Angular compiler when NgModules are optional or totally removed? If yes, why?

You must be logged in to vote
3 replies
@yharaskrik
Comment options

I am also very interested and curious what this RFC opens up in terms of build/compile time improvements and new technologies that can be leveraged.

@chaosmonster
Comment options

This is a great question, especially as I think the current approach looks more like a replacement for NgModules (which again was denied), so this would add more complexity as well.

@alxhub
Comment options

alxhubMay 3, 2022
Collaborator Author

Sorry for the slow answer here :)

For anything SSR/runtime performance related, standalone won't make any difference. It also won't make any difference in build performance within the current compiler architecture.

But, NgModule has a really annoying property:

@Component({selector:'blah-cmp',template:'<foo-cmp></foo-cmp>',})exportclassBlahCmp{}

What is<foo-cmp>? Who knows! Without scanning thewhole rest of the program to find the@NgModule that declaresBlahCmp, it's impossible to tell.

This isn't an issue in the current compiler, which always looks at the whole rest of the program because that's how TypeScript itself works. The current compiler also does a lot of bookkeeping to track the relationships between NgModules and components, so it can efficiently recompile the right things if e.g. the NgModule changes. This keeps incremental builds fast - there's no major gains here fromstandalone.

Where thisdoes start to get interesting is in hypothetical future compilation approaches. Some modern JS transpilers (esbuild, swc, etc) use file-by-file transpilation, skipping TypeScript's type operations, and can achieve parallel compilation and other neat tricks. This isn't possible with Angular todaybecause of NgModules and this weird backwards dependency arrow, where components don't have direct imports to their own dependencies.standalonedoes make compiling Angular with this kind of architecture actually possible.

Comment options

First of all a great RFC and generally standalone components are a blessed change!

One thing that was brought up in the previous RFC and I can't recall it being answered is the case of NgModules as logical grouping of related components. To be more precise - if a standalone component has some repeating pattern which fits to a new sub component - how can we make this new sub component privately usable only within the original component? With modules we would just omit it from the exports array. It seems in standalone components this is not feasible unless used in a library scope. I don't think exposing each and every component as a public standalone component even in apps is a good idea (same as private class methods).

Any direction on how to solve it? Or is it only me that thinks it's a problem?

You must be logged in to vote
4 replies
@mikezks
Comment options

You can tackle this directly with ECMAScript language features. Just create a subfolder for your module-scoped implementations and create anindex.ts file there, that defines what gets exported from that subfolder. The non-exported implementations are internal. Then, as a general rule, external implementations always import directly from that subfolder and never from paths inside.

@IgorMinar once mentioned on twitter, that those barrel-file-imports may directly be supported inside the new Component's decoratorimports: [ ].

So, to sum it up, it is not exactly the same separation, but quite close and as a bonus, it uses direct JS language features.

@pkozlowski-opensource
Comment options

@mikezks is totally right here - ECMAScript language features are the way to go!

@krilllind
Comment options

@mikezks I think using native barrel-file imports is only solving part of the problem here. In large codebases it's very common that child component share the same name as other child components in another module. With modern tools like VSC allowing for automatic imports, components could easily be imported by accident from the wrong path.

With currentNgModule declarations and exports feature, developers will instantly get IDE feedback as decelerated component (or for standalone component theimports property) is not valid and was not intended to be shared.

However without this metadata, you would only get this information at runtime when you realize that the wrong component was imported.
I would very much like to see a way of forcing a component to be private for it's parent. That way large component libraries likedevextreme,kendo UI and others can protect internal components.

@mikezks
Comment options

@krilllind, you can use the same strategy as with NgRx namespace imports likeimport * as MyModule from '../my-module';. So, as mentioned, still all current patterns are covered.

Comment options

This RFC is just incredible -- thank you! -- and will clearly have a positive impact on many aspects, from developer experience to sustainability.

I see Standalone Components as a great opportunity to improve the mental model of Angular applications. However, I feel like we are trying delegate a lot to the Router and I'm not convinced that it should be responsible for all.

Maybe I'm totally wrong but why couldn't we keep some of those properties/metadata in the components themselves? It would allow us to:

  1. Automatically detect routes,
  2. Respect the scope and responsibility of components at component level
  3. Children would be only dependent on their parent

I know this goes totally against the current project and router but like I said, this RFC is great opportunity to discuss.

Example:

// my-routed-component.ts@Component({path:'something',strategy:'lazy'// 'eager'children:[MyChildrenComponent,MyChildrenComponentFoo,MyChildrenComponentBar    ...]})exportclassMyRoutedComponent{}// routes.ts, if anyexportconstRoutes=[MyRoutedComponent,MyRoutedComponentFoo,MyRoutedComponentBar  ...]

Where routed components may have slightly more properties than they do now but it makes sense to me that these belong to the component instead of the Router. Also, routed component don't need aselector, hence would be replaced by thepath.

And whereRoutes is more of a pseudo lookup. Actually, maybe we don't even need it.

Another example

// my-route-config.tsexportconstMyRouteConfig=[{path:'something',strategy:'lazy'// 'eager'children:[],  ...}]// my-routed-component.ts@Component({route:MyRouteConfig,  ...})exportclassMyRoutedComponent{}

I have this feeling where the RouterModule could totally lookup routes directly from the components so we don't have to declare them anywhere.

This is me thinking outloud but as mentioned, I think this RFC is a good opportunity to discuss.

You must be logged in to vote
6 replies
@tayambamwanza
Comment options

This might not be the optimal solution, but I actually like the idea of component containing metadata of whether it should be lazy loaded or not and what path it belongs too then just passing the component to a consumer that would read those properties.

So when working on that component I could see any route metadata there and then rather than opening up the "routing file". That's just me though.

@pkozlowski-opensource
Comment options

Just a quick note: we did not intend to change how the router configuration works with@Component configuration. What we are doing here is a set of progressive changes to make standalone components usable with router and routing configuration being possible without NgModules.

We can certainly discuss the router <-> component interplay and I do here your ideas / concerns but this is out of scope of this particular RFC. There are probably tons of considerations when it comes to router and components interactions so it would definitively require a specific and detailed design.

@eskwisit
Comment options

Wow, awesome guys, I feel a little less wrong.

@krilllind totally, this was a very broad idea but overall, it would be really interesting to have the router looking up path instead of declaring every route and I'm sure it feasable without changing too much of the RouterModule, just the logic that receive the path. Regarding lazy load or eager, this could be something moved to the builder without necessarily have to load the component at runtime to know about the route but yeah, like I said, it's a very broad idea of an eventual RouteModule automatic route lookup, idk.

@tayambamwanza thanks mate, I guess I was a little off topic but all the discussion around the routes drawn my attention and this bumped into my head 😅

@pkozlowski-opensource I totally gotcha, I've been digressing from the RFC, it's just that it's a little scary to have many properties on the route list but this is just me.

@michaelfaith
Comment options

I guess where I question that approach just at a glance is the impact that keeping routes with the components would have on flexibility and potential for re-use. If I built a form component that depending on the circumstances could be used in a (child) route situation, in some segment of my page, but could also be displayed in a modal on a different screen or for a different use case, that doesn't really work if you've embedded route information into the component. Decoupling routes from components allows for more re-use options and keeps the components you build agnostic of where exactly they're being used.

@eskwisit
Comment options

@michaelfaith that's true, I thought about that too but, from my experience, most routed components (modules ftw) are not made to be re-usable. Also, there's always trade-offs and things to be cleared, the base idea is to avoid redundant coding practices and have the Router discovering routes he has to serve while keeping the responsibility of the path and some other properties at component level, that's actually quite what we do with NgModules at the moment where children belong to the module and not directly the router. But I agree, this is very broad and eventually needs clearance.

Comment options

@alxhub@pkozlowski-opensource
If have some additional concerns around:

Should the router automatically recognize and dereference default exports in its lazy loading APIs?

There is some discussion in this RFC where some peole think this is "not the best idea"™️. I do disagree, and think it might be the case for those people and should be taken care of with an optioal lint rule.
However, by asking this question, it looks like this is an optional thing the Angular team should add.
Isn't this plain es5 modules? I mean the import function will return apromise<defaultEmport>()
Is there a way to diffentiate that from:

// In someModue:exportdefaultconstsomeThing=classmyModule{/* code here */}// in the importing moduleconstmyModule=import('someModule').then(m=>m.someThing)// VersusconstmyModule1=import('someModule'))

They will both represent the same export. That made me curious. Is there a way to differentiate?
Or to put it differently, is there a way to prevent using default exports in the router config, to begin with?

EDIT: After rereading this, I thought it might come of as snarky. Sorry for that, that isn't the case.
As a library author, I'm really interested to find outif there is a way to differentiate, and when there is:How??

You must be logged in to vote
8 replies
@SanderElias
Comment options

@pkozlowski-opensource Ok just did are-read of the spec. I think it will bevery hard to tell the difference. As far as I can see, the things that make it different are not exposed to the runtime. Still would be interested, it might be something that becomes useful to me!

@mikezks
Comment options

@pkozlowski-opensource I am very sorry to hear, that default exports are not supported at least at the beginning. I understand the concerns mentioned in this RFC, but those new standalone APIs offer that much clean concepts back to the roots of JS/TS that is really a pity, that we do not support this plain language feature here. People that do not like that, should use linting rules to disallow this in their codebase.

@pkozlowski-opensource
Comment options

@mikezks just at the risk of over-communicating: you can absolutely use default exports toexport routes config. We just don't plan to do (at least not initially) and special treatment of those exports when using standard JSimport(....) syntax. So I believe that it is actually as close to JS/TS roots as possible :-)

@mikezks
Comment options

Yes, you are right,.then(esm => esm) would still work, right?
So, to be precise, my argument was a little wrong - nevertheless, hopefully we get support for a solution w/o the code above (optional of course).
I agree, a little magic, but a lot of people also like unicorns. 🦄 😁

@alxhub
Comment options

alxhubMay 3, 2022
Collaborator Author

.then(esm => esm) would still work, right?

Nope, sadly. Dynamic import (import()) doesn't automatically return the default export, it still gives the module object. You have to dereference it still:.then(esm => esm.default).

This is the "magic" that we're talking about adding - the router could look for adefault property on the result ofloadChildren orloadComponent and automatically unwrap it if it finds one.

Comment options

I'm assuming this is what you mean by the constructor anti-pattern? Yes, it would be great if I could have a function that just runs this code so I don't have to do this sweet NgModule constructor trick. Import this module and I have NgIdle up and running.

@NgModule({  imports:[    NgIdleModule.forRoot(),    WindowsModule  ],  declarations: [InactivityWarningComponent]})export class NgIdleStartUpModule {  constructor(private startUp: NgIdleStartUpService, private oidc: OidcClientService, private window: Window) {    if(!oidc.inIframe()) {      if(!window.opener) {        startUp.setMainWindowIdleCountdown();      } else {        startUp.setChildWindowIdleCountdown();      }    }  }}
You must be logged in to vote
2 replies
@pkozlowski-opensource
Comment options

This is whatINJECTOR_INITIALIZER is meant for. Does it work for your use-case?

@bh3605
Comment options

Well, it's not available to use yet is it? Maybe, it would look something like this?

{  provide: INJECTOR_INITIALIZER,  multi: true,  deps: [OidcClientService, Window, NgIdleStartUpService],  useValue: () => inject(AServiceInsteadOfAModuleClass).markLoaded()}

Where exactly would this code go? Do I now move this to the constructor ofAServiceInsteadOfAModuleClass?

    if(!oidc.inIframe()) {      if(!window.opener) {        startUp.setMainWindowIdleCountdown();      } else {        startUp.setChildWindowIdleCountdown();      }    }

There's also the import ofNgIdleModule.forRoot(). That needs to be called to have the NgIdle class available in the Injector. Would I need to specify any dependenciesNgIdleStartUpService uses? It's NgIdle, Window, and a reference to the document using@Inject(DOCUMENT).

Comment options

INJECTOR_INITIALIZER is lovely! 🥰

What about services that are:

  • neither provided in root
  • nor coupled to routing
  • nor provided by a component.

I think that I missed something 🤔

Here is an example to make this clear

Challenge: Wrapping non-treeshakable, or module-depending services

The following pattern is useful in order to hide implementation details (i.e. dependencies).
Users just have to importNotifierModule when they need to use theNotifier service.

@Injectable()exportclassNotifier{}@NgModule({providers:[Notifier],imports:[MatDialogModule,MatToastModule],})exportclassNotifierModule{}

A common similar scenario is the facade pattern with state management so that we don't forget to import the feature store before using the facade.

@Injectable()exportclassMyFacade{}@NgModule({providers:[MyFacade],imports:[StoreModule.forFeature(...)],})exportclassMyFacadeModule{}

These modules could be used this way

@Component({standalone:true,imports:[MyFacadeModule,NotifierModule]})exportclassMyComponent{}

Possible full standalone (but not equivalent) solutions with the current RFC

What would be the full-standalonish alternative?

classNotifier{staticproviders=[Notifier,importProvidersFrom(MatDialogModule), ...];}classMyFacade{staticproviders=[MyFacade,importProvidersFrom(StoreModule.forFeature(), ...]};

but then, where should we provide them?

a. in root injector withbootstrapApplication(App, {providers: [Notifier.providers]} and that would break laziness, modularity, and tree-shaking.

b. in the routingroutes = [{path: 'admin', providers: [MyFacade.providers]}...] and it couples component implementation details to the routing.

c. in the component@Component({providers: [Notifier.providers]}) and it would provide the services in each component injector and consume a bit more resources + causing some trouble if a service is stateful.

None of these solutions are equivalent to@Component({ imports: [MyFacadeModule, NotifierModule] }) which is lazy, modular, and tree-shakable while providing the services in the root or route injectors instead of component injector.

Proposition 1: Import services

Declaration

@Injectable({imports:[MatDialog,MatToast],})exportclassNotifier{}@Injectable({imports:[StoreModule.forFeature(...)]})exportclassMyFacade{}// or with providers@Injectable({// these providers are provided wherever MyFacade is providedproviders:[configureFeatureStore()],// Returns `{provide: INJECTOR_INITIALIZER, ...}` etc...})exportclassMyFacade{}

Usage

@Injectable({imports:[Notifier]})exportclassMyService{}@Component({imports:[MyFacade,Notifier]})exportclassMyComponent{}

imports andprovidedIn should probably be mutually exclusive to avoid tricky behaviors.

Proposition 2 : Fix it with docs

Document this use case in the list of use cases that still needNgModules

Propositon 3

👇 Please type your answer in the "Write a reply" field below

You must be logged in to vote
3 replies
@Harpush
Comment options

I just recently posted a feature request to allow providers inside injectables... Now that you mention it in the standalone context it seems to me even more correct...
For reference this is the issue I opened:#45832

@alxhub
Comment options

alxhubMay 3, 2022
Collaborator Author

Hi@yjaaidi,

What about services that are:

neither provided in root
nor coupled to routing
nor provided by a component.

In that case, you can expose a providers array:

exportconstNOTIFIER_SERVICES=[importProvidersFrom(StoreModule.forFeature(...)),NotifierService,];

Users can then importNOTIFIER_PROVIDERS into whatever context in their application makes sense.

I understand that this approach seems more burdensome than puttingNotifierModule in@Component.imports and letting the standalone injector handle creating it. Speaking of - it doesnot end up in the root or route injector in this case, it ends up in a specially created "standalone injector" for the component type in a given context.

This explicit separation between DI and components though is exactly the intention of standalone: separating the configuration of DI (via providers) from the configuration of components (via imports). It's very intentional that components do not bring their own DI configurationfor the rest of the application. In this way, there's a separation of concerns between components and the rest of the app, and components aren't responsible for configuring things bigger than themselves.

@yjaaidi
Comment options

Thx@alxhub for the clear response.

This paragraph is a precious gem that should be part of the upcoming docs 😉

This explicit separation between DI and components though is exactly the intention of standalone: separating the configuration of DI (via providers) from the configuration of components (via imports). It's very intentional that components do not bring their own DI configuration for the rest of the application. In this way, there's a separation of concerns between components and the rest of the app, and components aren't responsible for configuring things bigger than themselves.

Comment options

Demo repo

Hey, I set up this demo repo which uses the Standalone APIs preview (currently@angular/core@14.0.0-next.15) and is deployed to GitHub Pages:https://github.com/LayZeeDK/ngx-zippy-standalone

  • bootstrapApplication
  • Standalone Components
  • Standalone Directives
  • Standalone Pipes
  • Content projection
  • OnPush change detection
  • TestBed.createComponent(myStandaloneComponent)
You must be logged in to vote
4 replies
@michaelfaith
Comment options

Nice demonstration. Thanks for putting that together.

@pkozlowski-opensource
Comment options

Very nice@LayZeeDK - thnx for putting it together. If there would be anything to suggest I would consider demonstrating the fact that one can import existingNgModules to bring directives to a component dependencies (ex.CommonModule to bringngIf).

Other thing that I'm seeing people tripping over is DI configuration in theapplicationBootstrap - especially the usage withimportProvidersFrom.

But yeh, those initial demos are so important so thnx so much@LayZeeDK for doing this one!

@tayambamwanza
Comment options

@LayZeeDK can you please also add a stackblitz link.

Having seen this demo I'm a bit more comfortable with this change. Although maybe just call zippy-app app, because I think it would help to make it look as familiar as possible, if I see plain app.component.ts there. Then also as mentioned an example of importing a module, or at least a seperate demo if you want this one not to have any modules.

@e-oz
Comment options

@tayambamwanza not a stackblitz, but:https://layzeedk.github.io/ngx-zippy-standalone/

Comment options

  1. Should the router automatically recognize and dereference default exports in its lazy loading APIs?

No, please don't add any magic. IDE will not recognize this convention, part of developers will be confused as well.

You must be logged in to vote
0 replies
Comment options

I've tried to convert one of my components (which is "scam") into standalone components and found this issue:

I'm using NgRx ComponentStore, code initially was:

@Component({selector:'scs-editor',templateUrl:'./editor.component.html',styleUrls:['./editor.component.scss'],changeDetection:ChangeDetectionStrategy.OnPush,providers:[EditorStore]/// 👈 will initialize a new EditorStore instance})exportclassEditorComponent{publicreadonlystate$:Observable<EditorState>;publicreadonlytextCtrl:FormControl<string|null>;constructor(privatereadonlystore:EditorStore,){this.state$=this.store.state$;this.textCtrl=this.store.textCtrl;}save(){this.store.save$();}localModelDataLoaded(modelData:string){this.store.patchState({model:modelData});}}@NgModule({imports:[CommonModule,ReactiveFormsModule,MatInputModule,MatButtonModule,MatIconModule,MatProgressSpinnerModule,LocalStorageMenuComponentModule,],providers:[AppStore],/// 👈 will use existing AppStore instancedeclarations:[EditorComponent],exports:[EditorComponent],})exportclassEditorComponentModule{}

I've replaced it with:

@Component({selector:'scs-editor',templateUrl:'./editor.component.html',styleUrls:['./editor.component.scss'],changeDetection:ChangeDetectionStrategy.OnPush,standalone:true,providers:[EditorStore,AppStore],/// 👈 it's obviously not the same, how should I write it?imports:[CommonModule,ReactiveFormsModule,MatInputModule,MatButtonModule,MatIconModule,MatProgressSpinnerModule,LocalStorageMenuComponentModule,],})exportclassEditorComponent{publicreadonlystate$:Observable<EditorState>;publicreadonlytextCtrl:FormControl<string|null>;constructor(privatereadonlystore:EditorStore,){this.state$=this.store.state$;this.textCtrl=this.store.textCtrl;}save(){this.store.save$();}localModelDataLoaded(modelData:string){this.store.patchState({model:modelData});}}

As you can see, I'm accessing the global state store using "providers" of NgModule and creating a local store using "providers" of the component.

Maybe it's just some misunderstanding of the APIs - please let me know how to fix it :)

Code runs fine, but the AppStore is not initialized ;)

You must be logged in to vote
8 replies
@e-oz
Comment options

I have a single-component-module in a library. I can use NgModule.providers to make it's store globally accessible. I can not access bootstrapApplication to do this - as I understand, this function should be called once.

What do you mean by "not initialized", exactly?

https://ngrx.io/guide/component-store/initialization - please take a look. Sorry, this night I'll have time to create a stackblitz.

@pkozlowski-opensource
Comment options

I have a single-component-module in a library. I can use NgModule.providers to make it's store globally accessible. I can not access bootstrapApplication to do this - as I understand, this function should be called once.

Right, as a library author you can't callbootstrapApplication on the application's behave. I was brining up this scenario in the non-library context.

As for libraries - the suggested way of doing it would be to convertAppStore into a tree-shakable provider and provide it in root, ex.:

@Injectable({provideIn:"root"})classAppStore(){}

This is the pattern we've been suggesting for quite some time (and will probably double-down on this recommendation).

@e-oz
Comment options

@pkozlowski-opensource it works great. I was afraid that because of "{providedIn: 'root'}" ngOnDestroy will not be triggered, but it works fine. Thank you!

@e-oz
Comment options

I've converted a few components, and I can tell that it removes a LOT of boilerplate :)

@pkozlowski-opensource
Comment options

Yay, great to hear this!

Comment options

tl;dr; thank you for all the feedback - our intention is to implement APIs described in this RFC and roll them out in developer preview for Angular v14!

Shipping it as a developer preview in Angular v14!

First of all, we would like to thank everyone who commented on, asked questions, or otherwise engaged in the discussion on this RFC. The high quality input from the community sparked lots of useful discussions and allowed us to make adjustments to the initial design.

Based on all the comments and feedback we didn't find any use-cases or technical constraints that would "break" the design and / or prevent us from makingNgModules optional. As such we intend to implement and roll out new APIs in developer preview for Angular v14.

Components and applications

We’ve noticed multiple discussion threads centered around the “application” vs. “component” responsibilities as well as dependency injection.

We can clearly recognize the desire of shifting even more responsibilities to the components (ex. full DI configuration, including router configuration). But we believe that the concept of the "Angular application" is an important one and we want to preserve it -@alxhub wrote adetailed comment describing our thinking. Wedo hear loud and clear that the component / application distinction might not always be clear and we need to do better explaining and documenting the design.

We also didn't plan to re-design the DI system as part of the "standalone" efforts. Angular always had two types of injectors (so-called "module" injector and "node" injector) and it continues to be the case with standalone components.

Community feedback

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:

  • keep thebootstrapApplication name to properly reflect the job performed by this function and emphasize the "application" concept;
  • not add additional options to thebootstrapApplication function - while the were suggestions of zone-related configuration options we believe that it would be too confusing for the majority of users - we can always add more options based on the demand;
  • not support bootstrap of multiple components from thebootstrapApplication API - while several people pointed out this restriction, no use cases emerged for this functionality which were not well served by other APIs.
  • not add default imports unwrapping to the router'sloadComponent andloadChildren functions - we got mixed signals from the community and at the moment we prefer to stick to more verbose but clear and explicit API. We plan to revisit the advantages and disadvantages ofdefault exports outside the context of standalone.
  • add type-level restrictions toimportProvidersFrom to prevent it being misused to importNgModules into@Component providers.
  • RenameINJECTOR_INITIALIZER toENVIRONMENT_INITIALIZER.

Frequently asked questions

Finally, we’ve identified some recurring questions and would like to highlight answers here.

Import package forbootstrapApplication

The idea is to have it imported from the relevant platform, ex.:import {bootstrapApplication} from "@angular/platform-browser" - more details inthis thread;

SSR and standalone components

We are exposing the renderApplication API from the platfrom-server as a way of bootstrapping a standalone component for the server-rendering purposes.

Other APIs

We can think of a number of other API simplifications that are possible with standalone components. More specifically,TestBed and Angular elements APIs should be reviewed and adapted. So far we haven't had enough time to explore and settle on the details. We intend to cover additional APIs in a separate RFC.

More feedback welcomed!

We are excited to make those APIs available as developer preview in the upcoming v14 release and we'd love to hear more feedback once you get a chance to try them in your projects!

You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
RFCs
Labels
None yet
32 participants
@alxhub@e-oz@robwormald@flensrocker@pkozlowski-opensource@wilmarques@SanderElias@vinagreti@manfredsteyer@jnizet@DmitryEfimenko@otodockal@evanfuture@yjaaidi@bh3605@Platonn@LayZeeDK@michaelfaith@immotus@yharaskrik@BioPhotonand others

[8]ページ先頭

©2009-2025 Movatter.jp