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:

  1. Convert all components, directives and pipes to standalone
  2. Remove unnecessary NgModule classes
  3. 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?

  1. Added standalone: true
  2. 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 ☕!

Subscribe to Newsletter

One update per week. All the latest posts directly in your inbox.