【问题标题】:Angular inject a component into an attribute directiveAngular 将组件注入到属性指令中
【发布时间】:2020-01-12 07:46:38
【问题描述】:

Tl;dr:如何提供可见组件作为指令的依赖项?自然,组件必须在指令之前初始化,但它必须与应用程序稍后在组件的 selector 上运行时显示的实例相同。


详情:

我的 app.component.html 的结构如下:

app.component.html

<app-navigation></app-navigation>
<router-outlet></router-outlet>

顶部有一个始终可见的导航栏。 &lt;router-outlet&gt; 始终显示当前活动的组件。

我现在想允许&lt;router-outlet&gt; 中呈现的组件修改导航栏的内容,例如显示适合当前活动组件的附加按钮。这应该与指令一起使用,如下所示:

some.component.html

<div *appTopBar>
  <button>Additional Button</button>
</div>

附加按钮现在应该出现在顶部的导航栏中。

appTopBar 指令如下所示:

top-bar.directive.ts

import {AfterViewInit, Directive, OnDestroy, TemplateRef} from '@angular/core';
import {AppComponent} from '../navigation/navigation.component';

@Directive({
  selector: '[appTopBar]'
})
export class TopBarDirective implements AfterViewInit, OnDestroy {

  constructor(private tmpl: TemplateRef<any>,
              private nav: NavigationComponent) {
  }

  ngAfterViewInit(): void {
    this.nav.setTopBarContent(this.tmpl);
  }

  ngOnDestroy(): void {
    this.nav.setTopBarContent(null);
  }
}

该指令对NavigationComponent有依赖,可以通过公开提供的方法setTopBarContent()向导航栏传递内容:

navigation.component.ts

import {Component, EmbeddedViewRef, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';

@Component({
  selector: 'app-navigation',
  templateUrl: './navigation.component.html',
  styleUrls: ['./navigation.component.scss']
})
export class NavigationComponent {

  @ViewChild('topBarContainer',{static: false})
  topBar: ViewContainerRef;
  topBarContent: EmbeddedViewRef<any>;

  constructor() {}

  /**
   * Set the template to be shown in the top bar
   * @param tmpl template, null clears the content
   */
  public setTopBarContent(tmpl: TemplateRef<any>) {
    if (this.topBarContent) {
      this.topBarContent.destroy();
    }
    if (tmpl) {
      this.topBarContent = this.topBar.createEmbeddedView(tmpl);
    }
  }
}

我遇到的第一个问题是,在初始化 TopBarDirective 时,NavigationComponent 依赖项还不可用。我收到以下错误:

错误错误:未捕获(承诺):NullInjectorError:

StaticInjectorError(AppModule)[TopBarDirective -> NavigationComponent]: StaticInjectorError(Platform: core)[TopBarDirective -> NavigationComponent]:

NullInjectorError: 没有 NavigationComponent 的提供者!

所以显然组件在指​​令之后被初始化并且还不可用。

我尝试将NavigationComponent 添加到AppComponentproviders 数组中,并且依赖注入现在起作用了:

@NgModule({
  declarations: [
    NavigationComponent,
    SomeComponent,
    TopBarDirective
  ],
  imports: [
    BrowserModule,
    CommonModule
  ],
  providers: [NavigationComponent]
})
export class AppModule { }

但是,现在似乎有两个 NavigationComponent 实例。我通过在NavigationComponentconstructor 中生成一个随机数并记录它来检查这一点。该指令肯定有一个与&lt;app-navigation&gt; 选择器显示的实例不同的实例。

现在我知道这种模式以某种方式起作用。前段时间我发现它是由一些 Angular 开发人员介绍的,但不幸的是我不再有源代码了。然而,工作版本显示AppComponent 中的内容,因此该指令仅依赖于AppComponent,它似乎首先被初始化。因此,不会发生整个依赖性问题。

如何确保提供给TopBarDirectiveNavigationComponent 实例与&lt;app-navigation&gt; 选择器中显示的实例相同?

【问题讨论】:

    标签: angular typescript angular-directive angular-dependency-injection


    【解决方案1】:

    我建议您为此创建一个服务,例如 TopbarService,就像这样。我们将使用 BehaviorSubject 来设置模板并发出它的最新值。

    @Injectable()
    export class TopbarService {
    
      private currentState = new BehaviorSubject<TemplateRef<any> | null>(null);
      readonly contents = this.currentState.asObservable();
    
      setContents(ref: TemplateRef<any>): void {
        this.currentState.next(ref);
      }
    
      clearContents(): void {
        this.currentState.next(null);
      }
    }
    

    现在在指令中注入这个服务并调用服务方法。

    @Directive({
      selector: '[appTopbar]',
    })
    export class TopbarDirective implements OnInit {
    
      constructor(private topbarService: TopbarService,
                  private templateRef: TemplateRef<any>) {
      }
    
      ngOnInit(): void {
        this.topbarService.setContents(this.templateRef);
      }
    }
    

    NavigationComponent组件中订阅内容behaviorsubject以获取最新值并设置模板。

    export class NavigationComponent implements OnInit, AfterViewInit {
      _current: EmbeddedViewRef<any> | null = null;
    
      @ViewChild('vcr', { read: ViewContainerRef })
      vcr: ViewContainerRef;
    
      constructor(private topbarService: TopbarService,
                  private cdRef: ChangeDetectorRef) {
      }
    
      ngOnInit() {
      }
    
      ngAfterViewInit() {
        this.topbarService
          .contents
          .subscribe(ref => {
            if (this._current !== null) {
              this._current.destroy();
              this._current = null;
            }
            if (!ref) {
              return;
            }
            this._current = this.vcr.createEmbeddedView(ref);
            this.cdRef.detectChanges();
          });
      }
    }
    

    这个组件的 HTML 在你放置模板的地方会是这样的。

    template: `
        <div class="full-container topbar">
          <ng-container #vcr></ng-container>
          <h1>Navbar</h1>
        </div>
    `,
    

    【讨论】:

    • 是的,这是我之前想到的,但我仍然想知道是否有任何方法可以将视觉 NavigationComponent 注入指令中。一定有办法告诉 DI 不要实例化第二个NavigationComponent,而是使用之前注入指令的那个?
    • 我相信在这种情况下直接在指令中注入组件可能不值得考虑。可能有一些令人信服的案例,但如果您看到最佳实践,则应该避免它。这就是为什么 Angular 将组件与服务区分开来以增加模块化和可重用性。在角度指南中,明确建议尽可能使用服务以充分利用 DI。
    • 接受这一点,因为注入组件可能是不好的做法,应该使用服务来解决。
    【解决方案2】:

    要将控制器注入其指令,请使用forwardRef

    组件定义

    @Component({
        //...,
        providers:[
        {
          provide: MyController,
          useExisting: forwardRef(() => MyController)
        }]
    })
    export class MyController {
        //...
    }
    

    指令定义

    @Directive({
        //...
    })
    export class MyDirective {
        constructor(private ctlr: MyController) { }
    }
    

    那个构造函数可能需要一个@Host();我没有测试过这段代码。

    【讨论】:

      猜你喜欢
      • 2016-06-10
      • 2016-09-14
      • 1970-01-01
      • 1970-01-01
      • 2020-11-14
      • 2016-08-14
      • 1970-01-01
      • 2016-07-27
      • 2020-09-05
      相关资源
      最近更新 更多