【问题标题】:How to access host component from directive?如何从指令访问主机组件?
【发布时间】:2018-02-11 09:01:29
【问题描述】:

假设我有以下标记:

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

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

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

【问题讨论】:

    标签: angular angular-directive


    【解决方案1】:

    这是从 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

    【讨论】:

      【解决方案2】:

      对于 Angular 12,this comment 为我指出了一个肮脏的解决方案的正确方向。我知道这在原则上不是解决这个问题的好方法,但我的用例需要能够在不知道写入时访问组件实例的情况下访问组件实例,因为多个模块之间存在关注点分离。

      TL;DR:

      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。此LContainer 在索引 0(内部为 the HOST constant)处有一个条目,即 LView if the container is on a component node

      反过来,LView 在位置 8 处有一个条目(内部为 the CONTEXT constant),如果附加到的组件是 non-root component element(例如 &lt;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 https://stackoverflow.com/questions/46014761/how-to-access-host-component-from-directive#comment119646192_48563965
      
          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;
        }
      }
      

      【讨论】:

        【解决方案3】:

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

        第二个优点是无需更改即可访问现有组件。但是您可能需要处理大量检查和特殊情况,具体取决于实际注入的组件。

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

        所以你的选择真的取决于你的用例。

        【讨论】:

          【解决方案4】:

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

          &lt;my-comp myDirective [hostComponent]="hostComponent"&gt;&lt;/my-comp&gt;

          在 myCompComponent.ts 中,

          hostComponent = this;

          在 myDirective.ts 中,

          @Input() hostComponent: Component;

          【讨论】:

            【解决方案5】:

            注意:这是 hacky 并且可能不会在 Angular 的未来版本中工作。 在 Angular 10 中,我可以像这样访问主机组件:

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

            constructor(_viewContainerRef: ViewContainerRef)

            像这样获取宿主组件:

            let hostComponent = (&lt;any&gt;_viewContainerRef)._lContainer[0][8];

            【讨论】:

              【解决方案6】:

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

              @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'" ....>
              

              【讨论】:

                【解决方案7】:

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

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

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

                【讨论】:

                  【解决方案8】:

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

                  export class FromItemComponentBase  {
                    constructor(private hostElement: ElementRef) {
                      hostElement.nativeElement.__component=this;
                    }
                  }
                  

                  @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;
                    }
                  }
                  

                  【讨论】:

                  • @SlavaFominII 请仔细看,没有使用私有API
                  • 我保持正确,抱歉。 __component 的样子让我很困惑。
                  【解决方案9】:

                  如果您想在自定义组件上使用属性指令,您可以让这些组件从抽象类扩展,并将抽象类类型“forwardRef”到您的组件类型。这样您就可以在抽象类上(在您的指令中)进行 Angular 的 DI 选择。

                  抽象类:

                  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... :-)
                    }
                  
                  }
                  

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

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

                  【讨论】:

                  • 这对我来说似乎是完美的解决方案,它有什么缺点吗?
                  • 这是我使用指令引用任何组件所需要的
                  • 记得让抽象类可注入,否则你的指令会出错。例如,将@Injectable({ providedIn: 'root'}) 作为属性添加到您的抽象类
                  • 一些注意事项: 1. 有效! 2. forwardRef 实际上不是必需的,至少在最新版本的 Angular 中是这样。 4. 可以将抽象类作为接口使用:class MyCustomComponent extends AbstractCustomComponent implements MyReference, OnInit 3. Alf Moh 说的没必要……
                  【解决方案10】:
                  constructor(private vcRef: ViewContainerRef){
                          let parentComponent=(<any>this.vcRef)._view.context;
                  }
                  

                  【讨论】:

                  • 虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
                  • _view.context 暗示 _view 应该是私有的,因此您似乎是以非标准方式执行此操作,就像以后遇到此问题的任何人一样。引用:stackoverflow.com/questions/4484424/…
                  【解决方案11】:

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

                  在构造函数中注入ViewContainerRef

                  constructor(private _viewContainerRef: ViewContainerRef) { }
                  

                  然后使用

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

                  【讨论】:

                  • 从 Angular 9.0.0-next.3 开始,此解决方案不再有效。你知道他们现在可能把它藏在哪里了吗?
                  • @Fredrik_Macrobond .last(this.viewContainerRef['_hostView'][0].__ngContext_)
                  • 有人找到了适合 Ivy 的解决方案吗? Angular10
                  • Angular 12 的一个非常肮脏的解决方案:(this.viewContainerRef as any)._hostLView[8]。 8 指github.com/angular/angular/blob/… 中的CONTEXT(内联),请谨慎使用。
                  【解决方案12】:

                  你可以注入它

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

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

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

                  【讨论】:

                  • 谢谢你,Günter,它有效。理想情况下,我需要一个适用于任何组件的通用解决方案。事实上,您可能会对我在这里尝试做的事情提出建议:stackoverflow.com/questions/46014977
                  • 很多人都要求一个通用的解决方案(正如您在链接问题中看到的那样),但目前没有简单的解决方案。
                  • @GünterZöchbauer 我想我们可以使用一个接口,例如constructor(private host: HostedComponentInterface){} 并要求指令的用户实现该接口??
                  • TypeScript 接口在运行时不存在,因此不支持 DI。
                  • @Emobe 我没有遇到很多找不到解决方案的情况。这是最难的之一。对于其他人来说,通常有很好的解决方法。大多数设计决策都是为了提高效率而做出的,我认为这是值得的。
                  猜你喜欢
                  • 2018-09-26
                  • 2016-12-26
                  • 1970-01-01
                  • 2015-10-23
                  • 1970-01-01
                  • 2020-05-29
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-08-02
                  相关资源
                  最近更新 更多