如何从指令访问主机组件?

How to access host component from directive?

假设我有以下标记:

<my-comp myDirective></my-comp>

有什么方法可以从指令访问组件实例

更具体地说,我希望能够从 MyDirective 访问 MyComponent 的属性和方法,理想情况下 无需向上面的 HTML 添加任何内容.

注射即可

class MyDirective {
  constructor(private host:MyComponent) {}

一个严重的限制是,你需要提前知道组件的类型。

另见 https://github.com/angular/angular/issues/8277
当您事先不知道类型时,它还提供了一些解决方法。

您的指令可以是可应用于您的任何组件的通用指令。所以,在那种情况下,在构造函数中注入组件是不可能的,所以这里有另一种方法可以做到这一点

在构造函数

中注入ViewContainerRef
constructor(private _viewContainerRef: ViewContainerRef) { }

然后使用

获取它
let hostComponent = this._viewContainerRef["_data"].componentView.component;

这是从 github 期中摘录的,效果很好。缺点是需要事先了解组件,但在您的情况下,您无论如何都需要知道您正在使用的方法。

import { Host, Self, Optional } from '@angular/core';
  
export class ExampleDirective {  
  constructor(
    @Host() @Self() @Optional() public hostCheckboxComponent : MdlCheckboxComponent,
    @Host() @Self() @Optional() public hostSliderComponent   : MdlSliderComponent
  ) {
    if(this.hostCheckboxComponent) {
      console.log("host is a checkbox");
    } else if(this.hostSliderComponent) {
      console.log("host is a slider");
    }
}

信用: https://github.com/angular/angular/issues/8277#issuecomment-323678013

constructor(private vcRef: ViewContainerRef){
        let parentComponent=(<any>this.vcRef)._view.context;
}

如果您想在自定义组件上使用属性指令,您可以让这些组件从抽象 class 和 'forwardRef' 抽象 class 类型扩展到您的组件类型。这样你就可以在摘要 class 上创建 angular 的 DI select(在你的指令中)。

摘要class:

export abstract class MyReference { 
  // can be empty if you only want to use it as a reference for DI
}

自定义组件:

@Component({
  // ...
  providers: [
    {provide: MyReference, useExisting: forwardRef(() => MyCustomComponent)}
  ],
})
export class MyCustomComponent extends MyReference implements OnInit {
// ...
}

指令:

@Directive({
  selector: '[appMyDirective]'
})
export class CustomDirective{

  constructor(private host:MyReference) {
    console.log(this.host);
    // no accessing private properties of viewContainerRef to see here... :-)
  }

}

这样您就可以在任何扩展您的抽象的组件上使用该指令 class。

这当然只适用于您自己的组件。

我喜欢这个,它适用于 Angular 9 和 11。

nativeElement 是一个DOM对象,我可以动态设置当前组件的

  1. nativeElement 上自定义 __component 字段的实例;

  2. Directve中访问nativeElement.__component

    导出class FromItemComponentBase { 构造函数(私有主机元素:ElementRef){ hostElement.nativeElement.__component=这个; } }


@Component({
  selector: 'input-error',
  templateUrl: 'component.html'
})
export class FromItemErrorComponent extends FromItemComponentBase {
  constructor(private hostElement: ElementRef) {
    super(hostElement);
  }
}

@Component({
  selector: 'input-password',
  templateUrl: 'component.html'
})
export class FromItemPasswordComponent extends FromItemComponentBase {
  constructor(private hostElement: ElementRef) {
    super(hostElement);
  }
}

@Directive({selector: 'input-error,input-password,input-text'})
export class FormInputDirective {
  component:FromItemComponentBase;

  constructor(private hostElement: ElementRef) {
    this.component=hostElement.nativeElement.__component;
  }
}

您可以使用 ViewContainerRef 访问宿主组件。

constructor(private el: ViewContainerRef) {}

ngOnInit() {
    const _component = this.el && this.el.injector && this.el.injector.get(MyComponent);
}

参考:https://angular.io/api/core/ViewContainerRef

使指令通用的一种可能的解决方法是将组件的模板引用作为@Input 传递给指令。这增加了一些额外的 html 但它比我尝试过的许多其他 hacks 效果更好。

@Directive({selector: '[myDirective]'})
export class MyDirective implements OnInit {
  @Input() componentRef: any;
  @Input() propName: string;

  ngOnInit(){
    if (this.componentRef != null) {
      // Access component properties
      this.componentRef[this.propName];
    }
 }
}

视图中的用法:

<!-- Pass component ref and the property name from component as inputs -->
<app-component #appComponentRef myDirective [componentRef]="appComponentRef" [propName]="'somePropInComponent'" .... >

也适用于其他指令。使用 @Directive 装饰器的 exportAs 属性 来获取对指令实例的引用。

<form #myForm="ngForm" myDirective [componentRef]="myForm" [propName]="'ngSubmit'" ....>

注意:这是 hacky,可能在 Angular 的未来版本中不起作用。 在 Angular 10 中,我能够像这样访问主机组件:

与@Sunil Garg 的解决方案类似,在指令的 ctor 中注入 ViewContainerRef

constructor(_viewContainerRef: ViewContainerRef)

像这样获取主机组件:

let hostComponent = (<any>_viewContainerRef)._lContainer[0][8];

在构造函数中有 viewContainerRef: ViewContainerRef 并有以下代码 (this.viewContainerRef as any)._hostLView[8] 完美运行。但它看起来很老套,因此最好将组件引用作为输入参数传递给指令,如下所述。

<my-comp myDirective [hostComponent]="hostComponent"></my-comp>

在myCompComponent.ts,

hostComponent = this;

在myDirective.ts,

@Input() hostComponent: Component;

我从这里尝试了两种解决方案:
Michiel Windey 的(使用 abstract class 作为将使用指令的组件的接口)和 Anthony 的(使用 @Host() @Self() @Optional())。
两者都适用于 Angular 11.
两者都不是 hacky,并且可能是面向未来的,这与使用未记录的私有字段的 hacky 解决方案不同……

第二种的优点是无需更改现有组件即可访问它们。但是您可能需要处理很多检查和特殊情况,具体取决于真正注入的组件。

第一种有要求修改组件的不便,但是你可以在接口(抽象class)中定义你将要在指令中使用的所有字段和方法,从而您可以使用单个参数访问它们,而无需检查注入的组件类型。

所以您的选择实际上取决于您的用例。

对于 Angular 12, 为我指出了正确的解决方案方向。我知道原则上这不是解决此问题的好方法,但由于跨多个模块的关注点分离,我的用例需要能够访问组件实例而不知道它在写入时是什么。

长话短说:

class MyDirective {
  constructor(private vcRef: ViewContainerRef) {}

  private getHostComponent(): any {
    return this.vcRef._lContainer[0][8];
  }
}

您可以访问 ViewContainerRef_lContainer 属性、which represents the state associated with the container. This LContainer has an entry at index 0 (internally the HOST constant) which is an LView if the container is on a component node

LView 又在位置 8 处有一个条目(内部 the CONTEXT constant) which is a reference to the component instance if the component this is attached to is a non-root component element(例如 <app-*)。 这意味着您可以通过访问 lContainer[HOST][CONTEXT].

来访问主机上下文组件

复制粘贴的长答案及解释:

class MyDirective {
  constructor(private vcRef: ViewContainerRef) {}

  private getHostElementFromViewContainerRef(): unknown | null {
    // TL;DR of the below method:
    // return this.vcRef._lContainer[0][8];
    // Inspired by 

    const vcRef = this.vcRef as any; // We're accessing private properties so we cast to any to avoid awkward TS validation issues

    // We fetch the component associated with the element this directive is attached to by navigating via the ViewContainerRef.
    // The VCRef contains a reference to the LContainer, which represents the state associated with the container:
    // https://github.com/angular/angular/blob/12.2.x/packages/core/src/render3/interfaces/container.ts#L65
    const lContainer = vcRef._lContainer;

    if (!lContainer) {
      return null;
    }

    // LView has all its elements defined as array elements, with keys hardcoded to numeric constants:
    // https://github.com/angular/angular/blob/12.2.x/packages/core/src/render3/interfaces/view.ts#L26-L57
    // We care about two of them:
    const HOST = 0; // https://github.com/angular/angular/blob/12.2.x/packages/core/src/render3/interfaces/view.ts#L29
    const CONTEXT = 8; // https://github.com/angular/angular/blob/12.2.x/packages/core/src/render3/interfaces/view.ts#L37

    // LContainer is an array, with the element at the HOST position being an LView if the container is on a Component Node.
    // This means that this may not work if this directive is declared on a native HTML element.
    // Note that LContainer uses the same indexes as LView, so it's the same HOST constant as declared in the LView interfaces file.
    // https://github.com/angular/angular/blob/12.2.x/packages/core/src/render3/interfaces/container.ts#L66-L72

    const lView = lContainer[HOST];
    if (!lView) {
      return null;
    }

    // For a non-root component, the context is the component instance.
    // So if this directive is correctly attached to an Angular Component (e.g. `<app-*`),
    // this array entry will contain the instance of that component.
    // https://github.com/angular/angular/blob/12.2.x/packages/core/src/render3/interfaces/view.ts#L173-L180
    const contextElement = lView[CONTEXT];

    return contextElement || null;
  }
}