Migrating Angular library to standalone
Migrating from NgModules to Standalone in Angular
In this post, I describe the process of migrating from NgModules to the Standalone API in Angular. I’ll walk through this process using an example: upgrading the ng-toggle-button library.
Migrating to standalone
You can use the Angular CLI to migrate your app or you can do it by manually. For more information, refer to the Angular Documentation. To start the migration, run the following schematic:
ng generate @angular/core:standalone
This schematic will walk you through several steps:
- Convert all components, directives and pipes to standalone
- Remove unnecessary
NgModule
classes - Change the main file to bootstrap the app using standalone APIs
When you run the schematic it ask you to do some of those steps, the schematic will apply changes to your codebase. After running it, be sure to lint and format your code and fix any posible issues.
Migrating the NgToggleComponent
Let’s look at how the NgToggleComponent
from the ng-toggle-button
library was originally defined:
@Component({
selector: 'ng-toggle',
templateUrl: './ng-toggle.component.html',
styleUrls: ['./ng-toggle.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgToggleComponent),
multi: true
}
]
})
After migration, the component becomes:
@Component({
selector: 'ng-toggle',
+ standalone: true,
+ imports: [CommonModule],
templateUrl: './ng-toggle.component.html',
styleUrls: ['./ng-toggle.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgToggleComponent),
multi: true
}
]
})
These two line were added:
standalone: true,
imports: [CommonModule],
ℹ️
CommonModule
can be replaced with just the specific Angular directives used.
Now the component is standalone and manages its own dependencies via the imports
array.
Updating the NgToggleModule
Originally, the NgToggleModule
looked like this:
@NgModule({
declarations: [NgToggleComponent],
imports: [
CommonModule
],
exports: [NgToggleComponent],
providers: [NgToggleConfig]
})
After migration:
@NgModule({
imports: [NgToggleComponent],
exports: [NgToggleComponent],
providers: [NgToggleConfig]
})
Here, the component is imported instead of declared. Since NgToggleComponent
now includes CommonModule
, we no longer need to import it again in the module.
✅ I kept the
NgToggleModule
so the library can still be used in both NgModule-based and Standalone applications.
Updating the Test File
Be sure to also update your testing file:
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [NgToggleComponent],
+ imports: [NgToggleComponent],
providers: [NgToggleConfig]
})
.compileComponents();
});
A note about the provider
In this library, we have the NgToggleConfig
provider class, but it is not provided in root. This means, in order to use it, you need to include it in the providers
array of the component or module where it’s needed.
However, you can make it globally available by updating it like so:
@Injectable({
providedIn:'root'
})
export class NgToggleConfig
Now you can inject it without need to specify in the providers. You can inject NgToggleConfig
to change the config of the ng-toggle-component
This is helpful if you want to provide a default configuration without repeating it in every usage.
Migrating the demo app
The same steps can be applied to the demo app.
The AppComponent
used to be
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
The updated AppComponent
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [CommonModule, NgToggleComponent, FormsModule, ReactiveFormsModule, JsonPipe],
})
What changed?
- Added
standalone: true
- Added required modules and pipes in the
imports
array
In the demo app, we are planning to completely remove
AppModule
.
Old main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
New main.ts
import { importProvidersFrom } from '@angular/core';
import { AppComponent } from './app/app.component';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [importProvidersFrom(BrowserModule)]
})
.catch(err => console.error(err));
Now you’re bootstrapping the app using the AppComponent
directly. AppModule
is no longer needed.
The importProvidersFrom()
function is automatically added when you run
ng g @angular/core:standalone
to bring in modules previously used in the root module.
Conclusion
That’s it! What do you think about the migration—was it easy, hard, or a mix of both?
Now, you can use the ng-toggle-button
library with the Standalone API or with traditional NgModules.
In this case, the migration was easy, but when migrating more complex applications, be mindful of which modules, directives, and pipes your components depend on. Also, take note if you’re using NgModules
for lazy-loaded routes—that will require additional planning.
If you enjoyed this post, consider buying me a coffee ☕!