Search code examples
angularangular-materialangular-directive

NullInjectorError: No provider for MatButton


I am create a button directive to show mat spinner inside mat button to show loading when api call and its working fine when i am pass [loading]="true" inside button tag but also show NullInjectorError: No provider for MatButton! error in console.

I am using lazy loaded component when I am load button directive inside login.module.ts and register.module.ts it show The Directive 'ButtonDirective' is declared by more than one NgModule.

so i want to remove no provider error and want to load directive in multiple modules and i am using angular v16+.

button.directive.ts

import { ComponentRef, Directive, Input, OnChanges, Renderer2, SimpleChanges, ViewContainerRef } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { ThemePalette } from '@angular/material/core';
import { MatProgressSpinner } from '@angular/material/progress-spinner';

@Directive({
  selector: `button`,
})
export class ButtonDirective implements OnChanges {
  private spinner!: ComponentRef<MatProgressSpinner> | null;

  @Input() loading = false;
  @Input() disabled = false;
  @Input() color: ThemePalette;

  constructor(
    private matButton: MatButton,
    private viewContainerRef: ViewContainerRef,
    private renderer: Renderer2
  ) {}

  ngOnChanges(changes: SimpleChanges): void {
    if (!changes['loading']) {
      return;
    }

    if (changes['loading'].currentValue) {
      this.matButton._elementRef.nativeElement.classList.add('button-loading');
      this.matButton.disabled = true;
      this.createSpinner();
    } else if (!changes['loading'].firstChange) {
      this.matButton._elementRef.nativeElement.classList.remove(
        'button-loading'
      );
      this.matButton.disabled = this.disabled;
      this.destroySpinner();
    }
  }

  private createSpinner(): void {
    if (!this.spinner) {
      this.spinner = this.viewContainerRef.createComponent(MatProgressSpinner);
      this.spinner.instance.color = this.color;
      this.spinner.instance.diameter = 20;
      this.spinner.instance.mode = 'indeterminate';
      this.renderer.appendChild(
        this.matButton._elementRef.nativeElement,
        this.spinner.instance._elementRef.nativeElement
      );
    }
  }

  private destroySpinner(): void {
    if (this.spinner) {
      this.spinner.destroy();
      this.spinner = null;
    }
  }
}

login.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';

import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ButtonDirective } from 'src/app/utility/button.directive';

@NgModule({
  declarations: [
    LoginComponent,
    ButtonDirective
  ],
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    LoginRoutingModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatIconModule
  ]
})
export class LoginModule { }

Solution

  • You buttonDirective should belong to a "dedicated" module. This can be, e.g. a utils module, e.g.

    @NgModule({
      imports: [..],
      declarations:[ButtonDirective,...]
      exports:[ButtonDirective,....]
    })
    export class UtilsModule{}
    

    Then import the UtilsModule in all the modules where you need the directive. You can check this link (Really in the officials docs are not very well explained)