Cover photo generated with Microsoft Designer.

In the past, developers had to include mixed Angular modules from Angular Material at the app's root level to supply the services that these components depend on.

Standalone Angular application

With the arrival of Angular 14.0, the importProvidersFrom function emerged, bringing support for standalone applications. This approach made the purpose of integrating the Angular module into the ApplicationConfig used alongside bootstrapApplication much clearer, as illustrated in the example below.

import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { MatNativeDateModule, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerModule} from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackbarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { appDateFormats } from './app-date-formats';

export const appConfig: ApplicationConfig = {
  providers: [
    // Datepicker (and Timepicker)
    importProvidersFrom(MatDatepickerModule, MatNativeDateModule),
    { provide: MAT_DATE_FORMATS, useValue: appDateFormats },
    importProvidersFrom(MatDialogModule),
    importProvidersFrom(MatSnackbarModule),
    importProvidersFrom(MatTooltipModule),
  ],
};
Enter fullscreen mode Exit fullscreen mode
app.config.ts example with Angular Material 14.0.

Classic Angular application

In a traditional Angular setup, the purpose behind keeping these module imports inside an AppModule isn't as obvious.

import { ApplicationConfig, importProvidersFrom, NgModule } from '@angular/core';
import { MatNativeDateModule, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerModule} from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackbarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { appDateFormats } from './app-date-formats';

@NgModule({
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  imports: [
    // Angular Material
    BrowserAnimationsModule,
    // Datepicker (and Timepicker)
    MatDatepickerModule,
    MatNativeDateModule,
    { provide: MAT_DATE_FORMATS, useValue: appDateFormats },
    MatDialogModule,
    MatSnackbarModule,
    MatTooltipModule,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode
app.module.ts example with Angular Material 13.3.

Standalone Angular Material providers

Newer releases of Angular Material let you drop these from your ApplicationConfig entirely, since everything they bring to the table is tree-shakeable — either via @Injectable({ providedIn: 'root' }) or through an InjectionToken that comes with its own inline provider.

When it comes to what the Datepicker and Timepicker need, you can turn to something like provideNativeDateAdapter or a date adapter specific to your date-time library of choice, such as provideDateFnsAdapter, as the example below illustrates.

import { ApplicationConfig } from '@angular/core';
import { provideNativeDateAdapter } from '@angular/material/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { appDateFormats } from './app-date-formats';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimations(),
    // Datepicker (and Timepicker)
    provideNativeDateAdapter(appDateFormats),
  ],
};
Enter fullscreen mode Exit fullscreen mode
app.config.ts example with Angular Material 17.1.

That reads a lot more cleanly, doesn't it?

Benefits

Beyond making the application setup code more concise and straightforward to follow, here is what we gain.

  • A leaner bundle, since Angular Material’s service dependencies only load when a lazy-loaded chunk first requests them
  • Cleaner Storybook component story definitions, which can be simplified to match our ApplicationConfig, as shown in the upcoming section
  • Easier component test configuration, streamlined in the same manner demonstrated further below

Storybook component stories

For component stories, the applicationConfig Storybook decorator brings in root-level providers.

import { provideNativeDateAdapter } from '@angular/material/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { Meta, StoryObj, applicationConfig } from '@storybook/angular';
import { myDateFormats } from './my-date-formats';
import { MyMaterialComponent } from './my-material.component';

const meta: Meta<MyMaterialComponent> = {
  title: 'MyMaterialComponent',
  component: MyMaterialComponent,
  decorators: [
    applicationConfig({
      providers: [
        // Angular Material
        provideAnimationsAsync(),
        // Datepicker (and Timepicker)
        provideNativeDateAdapter(myDateFormats),
      ],
    }),
  ],
};

export default meta;
type Story = StoryObj<MyMaterialComponent>;

export const Default: Story = {};
Enter fullscreen mode Exit fullscreen mode
my-material.component.stories.ts example with Angular Material 17.1.

Angular Material used to require a workaround: the importProvidersFrom helper, or listing the Angular module references in the imports array of the moduleMetadata Storybook decorator.

Component tests

Handling those dependencies in component tests is easier these days as well. When a component is tested in isolation, the root-level providers are set up through the providers configuration passed to the TestBed.configureTestingModule call.

import { TestBed } from '@angular/core/testing';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { myDateFormats } from './my-date-formats';
import { MyMaterialComponent } from './my-material.component';

it('MyMaterialComponent', () => {
  TestBed.configureTestingModule({
    providers: [
      // Angular Material
      provideNoopAnimations(),
      // Datepicker (and Timepicker)
      provideNativeDateAdapter(myDateFormats),
    ],
  });

  const fixture = TestBed.createComponent(MyMaterialComponent);

  expect(fixture.componentInstance).toBeDefined();
});
Enter fullscreen mode Exit fullscreen mode
my-material.component.spec.ts example with Angular Material 17.1.

Conclusion

Using Angular Material has become more straightforward in recent releases, largely thanks to the switch to standalone providers for component dependencies that began with version 17.1. Below, the table lists the precise version in which every Angular Material component adopted these standalone providers.

Component First version with standalone providers
Datepicker 17.1.0
Dialog 17.0.0
Snackbar 17.0.0
Timepicker 19.0.0
Tooltip 15.0.4

Configuring Angular Material components at the root provider level is more straightforward and simpler to understand, whether you're working with Angular applications, Storybook component stories, or Angular component tests.

The providers are tree-shakable, which keeps bundle sizes lean.

With the exception of the Date picker and Timepicker components, there's no need to remember to add any providers. And if we drop all Angular Material component usage from an app, story, or test, there's no need to remember to remove them either.

Angular and Angular Material have evolved through these stages over time.

  1. Including Angular modules in NgModule.imports for AppModule
  2. Feeding Angular modules to importProvidersFrom within ApplicationConfig.providers
  3. No Angular modules required—just a provider function for the Datepicker and Timerpicker dependencies