角度将组件注入属性指令

时间:2019-09-10 14:14:13

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

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


详细信息:

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

app.component.html

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

顶部始终有一个导航栏。 <router-outlet>始终显示当前活动的组件。

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

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(平台:核心)[TopBarDirective-> NavigationComponent]:

     

NullInjectorError:没有NavigationComponent的提供者!

很明显,该组件在指令后被初始化,尚不可用。

我尝试将NavigationComponent添加到providers的{​​{1}}数组中,并且依赖项注入现在可以工作:

AppComponent

但是,似乎现在有两个NavigationComponent实例。我通过在@NgModule({ declarations: [ NavigationComponent, SomeComponent, TopBarDirective ], imports: [ BrowserModule, CommonModule ], providers: [NavigationComponent] }) export class AppModule { } 的{​​{1}}中生成一个随机数并记录下来来进行检查。该指令肯定有一个与constructor选择器上显示的实例不同的实例。

现在我知道此模式可以以某种方式起作用。我是在某个Angular开发人员介绍它的前一段时间才找到的,但是很遗憾,我没有源代码了。但是,工作版本将内容显示在NavigationComponent中,因此该指令仅依赖于<app-navigation>,后者似乎首先被初始化。因此,不会发生整个依赖项问题。

如何确保提供给AppComponent的{​​{1}}实例与AppComponent选择器上显示的实例相同?

2 个答案:

答案 0 :(得分:0)

我建议您为此创建一个名为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>
`,

答案 1 :(得分:0)

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

组件定义

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

指令定义

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

该构造函数可能需要@Host();我尚未测试此代码。