Angular's official documentation instructs developers to place a specific snippet in the main.ts file to bootstrap an application:
platformBrowserDynamic().bootstrapModule(AppModule);
The expression platformBrowserDynamic() is responsible for establishing a platform. The Angular docs define a platform as:
the entry point for Angular on a web page. Each page has exactly one platform, and services (such as reflection) which are common to every Angular application running on the page are bound in its scope.
Angular also maintains a notion of an active application instance, which can be injected via the ApplicationRef token. A single platform can host multiple applications, each created by invoking bootstrapModule on a module. This is exactly what happens in the main.ts file — the statement first establishes the platform, then initializes an application instance.
During application creation, Angular inspects the bootstrap property of the module being used to start the app (in this case, AppModule):
@NgModule({
imports: [BrowserModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
This property typically points to the component that should be used as the root. Angular then locates the DOM element matching that component's selector and initializes it.
This flow presupposes that the root component is known ahead of time. However, consider a scenario where the server decides at runtime which component should bootstrap the application. How do you initiate the bootstrap once that information arrives? The solution turns out to be fairly simple.
NgDoBootstrap
Suppose we have two components, referred to as A and B. At runtime, we'll determine which one gets used. Let’s define these two components:
import { Component } from '@angular/core';
@Component({
selector: 'a-comp',
template: `<span>I am A component</span>`
})
export class AComponent {}
@Component({
selector: 'b-comp',
template: `<span>I am B component</span>`
})
export class BComponent {}
They are then registered within AppModule:
@NgModule({
imports: [BrowserModule],
declarations: [AComponent, BComponent],
entryComponents: [AComponent, BComponent]
})
export class AppModule {}
The key point here is that these components are not listed in the bootstrap property because we intend to bootstrap them manually. They must also appear in entryComponents so that the compiler generates factories for them. Angular automatically adds components declared in the bootstrap property to entry components, which is why the root component is rarely added there explicitly.
Since we don't yet know whether A or B will be selected, neither selector is placed in index.html; at this stage, it looks like this:
<body>
<h1 id="status">
Loading AppComponent content here ...
</h1>
</body>
If you attempt to run the app now, you'll encounter an error:
The module AppModule was bootstrapped, but it does not declare “@NgModule.bootstrap” components nor a “ngDoBootstrap” method. Please define one of these
Angular is essentially complaining that no root component was designated for bootstrap. Since we can't know which one to choose ahead of time, we will handle bootstrapping manually. To make that possible, we add an ngDoBootstrap method to AppModule:
export class AppModule {
ngDoBootstrap(app) { }
}
Angular supplies a reference to the running application — an instance of ApplicationRef — as an argument to this method. When the time comes to initialize the root component, we'll call the bootstrap method on that ApplicationRef.
Now let's define a helper method called bootstrapRootComponent to handle the task of bootstrapping the root component once it's known:
// app - reference to the running application (ApplicationRef)
// name - name (selector) of the component to bootstrap
function bootstrapRootComponent(app, name) {
// define the possible bootstrap components
// with their selectors (html host elements)
const options = {
'a-comp': AComponent,
'b-comp': BComponent
};
// obtain reference to the DOM element that shows status
// and change the status to `Loaded`
const statusElement = document.querySelector('#status');
statusElement.textContent = 'Loaded';
// create DOM element for the component being bootstrapped
// and add it to the DOM
const componentElement = document.createElement(name);
document.body.appendChild(componentElement);
// bootstrap the application with the selected component
const component = options[name];
app.bootstrap(component);
}
This method takes the ApplicationRef reference and the name of the component to bootstrap. Additionally, we've set up an options map containing all possible bootstrap components, using their selectors as keys. When the server provides the necessary information, we'll use it to look up the corresponding component class.
For demonstration, I've included a mock fetch function that simulates an HTTP call. After a 2-second delay, it resolves with the selector b-comp:
function fetch(url) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('b-comp');
}, 2000);
});
}
Now that our bootstrap function is ready, we can invoke it within the module's ngDoBootstrap method:
export class AppModule {
ngDoBootstrap(app) {
fetch('url/to/fetch/component/name')
.then((name)=>{ this.bootstrapRootComponent(app, name)});
}
}
That's all there is to it. Here's a working stackblitz example that illustrates the solution.
Does it work with AOT?
Absolutely. You simply need to precompile every component and use the resulting factories when starting the application:
import {AComponentNgFactory, BComponentNgFactory} from './components.ngfactory.ts';
@NgModule({
imports: [BrowserModule],
declarations: [AComponent, BComponent]
})
export class AppModule {
ngDoBootstrap(app) {
fetch('url/to/fetch/component/name')
.then((name)=>{ this.bootstrapRootComponent(app, name)});
}
bootstrapRootComponent(app, name) {
const options = {
'a-comp': AComponentNgFactory,
'b-comp': BComponentNgFactory
};
In this case, listing components in entryComponents is unnecessary because the factories already exist and require no further compilation.
