You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 16+按钮指令报错:NullInjectorError及多模块重复声明问题

解决Angular按钮指令的两个问题

问题1:NullInjectorError: No provider for MatButton!

原因

指令选择器为button,会匹配页面所有<button>元素,但只有带有mat-button(或Material其他按钮指令)的元素才存在MatButton提供者,普通按钮没有该提供者,导致注入失败。

解决方法

修改指令选择器,仅匹配Material按钮组件:

@Directive({
  selector: `button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab]`,
})

该配置会让指令仅作用于Material按钮,避免给普通按钮注入MatButton时出错。

如果需要兼容普通按钮(不推荐,因为指令逻辑依赖MatButton),可以给MatButton注入添加@Optional()装饰器:

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

问题2:The Directive 'ButtonDirective' is declared by more than one NgModule.

原因

Angular规则:一个指令只能被一个NgModule声明,多模块重复声明会触发该错误。

解决方法

创建共享模块(SharedModule),将ButtonDirective放入该模块声明并导出,其他模块只需导入SharedModule即可使用指令。

1. 创建SharedModule

// src/app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ButtonDirective } from '../utility/button.directive';

@NgModule({
  declarations: [ButtonDirective],
  imports: [
    CommonModule,
    MatButtonModule,
    MatProgressSpinnerModule
  ],
  exports: [ButtonDirective]
})
export class SharedModule { }

2. 修改LoginModule和RegisterModule

移除declarations中的ButtonDirective,改为导入SharedModule:

// 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 { SharedModule } from '../shared/shared.module';

@NgModule({
  declarations: [
    LoginComponent
    // 移除ButtonDirective的声明
  ],
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    LoginRoutingModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatIconModule,
    SharedModule
  ]
})
export class LoginModule { }

RegisterModule执行相同修改:移除ButtonDirective声明,导入SharedModule。

最终完整的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[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab]`,
})
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;
    }
  }
}

内容的提问来源于stack exchange,提问作者prashant kushwah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 17:30:57