【发布时间】:2018-02-11 09:01:29
【问题描述】:
假设我有以下标记:
<my-comp myDirective></my-comp>
有什么方法可以从指令访问组件实例?
更具体地说,我希望能够从MyDirective 访问MyComponent 的属性和方法,理想情况下无需在上面的HTML 中添加任何内容。
【问题讨论】:
假设我有以下标记:
<my-comp myDirective></my-comp>
有什么方法可以从指令访问组件实例?
更具体地说,我希望能够从MyDirective 访问MyComponent 的属性和方法,理想情况下无需在上面的HTML 中添加任何内容。
【问题讨论】:
这是从 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
【讨论】:
对于 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(例如 <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;
}
}
【讨论】:
我从这里尝试了两种解决方案:
Michiel Windey 的一个(使用 abstract class 作为将使用该指令的组件的接口)和 Anthony 的一个(使用 @Host() @Self() @Optional())。
两者都适用于 Angular 11。
两者都不是 hacky 并且可能是面向未来的,不像使用无证私有字段的 hacky 解决方案......
第二个优点是无需更改即可访问现有组件。但是您可能需要处理大量检查和特殊情况,具体取决于实际注入的组件。
第一个不方便需要修改组件,但是您可以在接口(抽象类)中定义您将在指令中使用的所有字段和方法,因此您可以使用单个参数,无需检查注入的组件类型。
所以你的选择真的取决于你的用例。
【讨论】:
在构造函数中有 viewContainerRef: ViewContainerRef 并有以下代码 (this.viewContainerRef as any)._hostLView[8] 完美运行。但它看起来很 hacky,所以最好将组件引用作为 Input 参数传递给指令,如下所述。
<my-comp myDirective [hostComponent]="hostComponent"></my-comp>
在 myCompComponent.ts 中,
hostComponent = this;
在 myDirective.ts 中,
@Input() hostComponent: Component;
【讨论】:
注意:这是 hacky 并且可能不会在 Angular 的未来版本中工作。 在 Angular 10 中,我可以像这样访问主机组件:
类似于@Sunil Garg 的解决方案,在指令的ctor 中注入ViewContainerRef:
constructor(_viewContainerRef: ViewContainerRef)
像这样获取宿主组件:
let hostComponent = (<any>_viewContainerRef)._lContainer[0][8];
【讨论】:
使指令通用的一种可能的解决方法是将组件的模板引用作为@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'" ....>
【讨论】:
您可以使用 ViewContainerRef 访问主机组件。
constructor(private el: ViewContainerRef) {}
ngOnInit() {
const _component = this.el && this.el.injector && this.el.injector.get(MyComponent);
}
【讨论】:
我喜欢这个,它适用于 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;
}
}
【讨论】:
__component 的样子让我很困惑。
如果您想在自定义组件上使用属性指令,您可以让这些组件从抽象类扩展,并将抽象类类型“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'}) 作为属性添加到您的抽象类
class MyCustomComponent extends AbstractCustomComponent implements MyReference, OnInit 3. Alf Moh 说的没必要……
constructor(private vcRef: ViewContainerRef){
let parentComponent=(<any>this.vcRef)._view.context;
}
【讨论】:
_view.context 暗示 _view 应该是私有的,因此您似乎是以非标准方式执行此操作,就像以后遇到此问题的任何人一样。引用:stackoverflow.com/questions/4484424/…
您的指令可以是可应用于任何组件的通用指令。所以,在那种情况下,在构造函数中注入组件是不可能的,所以这是另一种方法来做同样的事情
在构造函数中注入ViewContainerRef
constructor(private _viewContainerRef: ViewContainerRef) { }
然后使用
获取它let hostComponent = this._viewContainerRef["_data"].componentView.component;
【讨论】:
(this.viewContainerRef as any)._hostLView[8]。 8 指github.com/angular/angular/blob/… 中的CONTEXT(内联),请谨慎使用。
你可以注入它
class MyDirective {
constructor(private host:MyComponent) {}
一个严重的限制是,您需要提前知道组件的类型。
另见https://github.com/angular/angular/issues/8277
当您事先不知道类型时,它还提供了一些解决方法。
【讨论】:
constructor(private host: HostedComponentInterface){} 并要求指令的用户实现该接口??