17

I'm doing some tests with Angular 2 and I have a directive (layout-item) that can be applied to all my components.

Inside that directive I want to be able to read some metadata defined on the component but for that I need to access the component's reference.

I have tried the following approach but I was unable to get what I need. Does any one has a suggestion?

@Component({...})
@View({...})
@MyAnnotation({...})
export class MyComponentA {...}


// Somewhere in a template
<myComponentA layout-item="my config 1"></myComponentA>
<myComponentB layout-item="my config 2"></myComponentA>

// ----------------------

@ng.Directive({
    selector: "[layout-item]",
    properties: [
        "strOptions: layout-item"
    ],
    host: {

    }
})

export class LayoutItem {

    // What works
    constructor(@Optional() @Ancestor({self: true}) private component: MyComponent1) {

 // with the constructor defined like this, component is defined with myComponent1 instance.
Reflector.getMetadata("MyAnnotation", component.constructor); // > metadata is here!
    }

// What I needed
    constructor(@Optional() @Ancestor({self: true}) private component: any) {

 // This will crash the app. If instead of any I specify some other type, the app will not crash but component will be null.
 // This directive can be applied to any component, so specifying a type is not a solution. 
    }
}
jpsfs
  • 708
  • 2
  • 7
  • 24
  • You could use a service to pass from the component its property to the directive. Keep in mind [this](https://github.com/angular/angular/pull/3372) although I don't know if that's just renaming @Ancestor or changing its functionality. – Eric Martinez Jul 31 '15 at 17:08
  • Hi @Eric Martinez. Not sure how to do it with a service. Can you plz elaborate a bit? – jpsfs Jul 31 '15 at 19:47
  • See my answer, I made it without services. – Eric Martinez Jul 31 '15 at 20:54

6 Answers6

10

UPDATE:

Since Beta 16 there is no official way to get the same behavior. There is an unofficial workaround here: https://github.com/angular/angular/issues/8277#issuecomment-216206046


Thanks @Eric Martinez, your pointers were crucial in getting me in the right direction!

So, taking Eric's approach, I have managed to do the following:

HTML

<my-component layout-item="my first component config"></my-component>

<my-second-component layout-item="my second component config"></my-second-component>

<my-third-component layout-item="my third component config"></my-third-component>

Three different components, all of the share the same layout-item attribute.

Directive

@Directive({
  selector : '[layout-item]'
})
export class MyDirective {
  constructor(private _element: ElementRef, private _viewManager: AppViewManager) {
    let hostComponent = this._viewManager.getComponent(this._element);
    // on hostComponent we have our component! (my-component, my-second-component, my-third-component, ... and so on!
  }

}
jpsfs
  • 708
  • 2
  • 7
  • 24
5

Forget about the Service, there's a simpler form of doing this

Option 1 (Not what you need, but it may be useful for other users)

HTML

<my-component layout-item="my first component config"></my-component>

<my-second-component layout-item="my second component config"></my-second-component>

<my-third-component layout-item="my third component config"></my-third-component>

Three different components, all of the share the same layout-item property.

Directive

@Directive({
  selector : '[layout-item]',
  properties: ['myParentConfig: my-parent-config'] // See the components for this property
})
export class MyDirective {
  constructor() {

  }

  onInit() {
    console.log(this.myParentConfig);
  }
}

Pretty straightforward, not much to explain here

Component

@Component({
  selector : 'my-component',
  properties : ['myConfig: layout-item']
})
@View({
  template : `<div [my-parent-config]="myConfig" layout-item="my config"></div>`,
  directives : [MyDirective]
})
export class MyComponent {
  constructor() {
  }
}

I'm pretty sure that you understand this, but for the sake of a good answer I will explain what it does

properties : ['myConfig: layout-item']`

This line assigns the layout-item property to the internal myConfig property.

Component's template

template : `<div [my-parent-config]="myConfig" layout-item="my config"></div>`,

We are creating a my-parent-config property for the directive and we assign the parent's config to it.

As simple as that! So now we can add more components with (pretty much) the same code

Second component

@Component({
  selector : 'my-second-component',
  properties : ['myConfig: layout-item']
})
@View({
  template : `<div [my-parent-config]="myConfig" layout-item="my config"></div>`,
  directives : [MyDirective]
})
export class MySecondComponent {
  constructor() {
  }
}  

See? Was much easier than my idea of using services (awful but 'working' idea).

With this way it is much simpler and cleaner. Here's the plnkr so you can test it.

(It wasn't what you need :'( )

UPDATE

Option 2

For what I understood of your updated question is that you need a reference to the component, so what I came up with is pretty similar to my original answer

What I did :

  • First I made the components to hold a reference to themselves
<my-cmp-a #pa [ref]="pa" layout-item="my first component config"></my-cmp-a>
<my-cmp-b #pb [ref]="pb" layout-item="my first component config"></my-cmp-b>
<my-cmp-c #pc [ref]="pc" layout-item="my first component config"></my-cmp-c>
  • Then I passed each reference to the LayoutItem directive (which was injected in each component, not at top-level)
@Component({
  selector : 'my-cmp-a',
  properties : ['ref: ref']
})
@View({
  template : '<div [parent-reference]="ref" layout-item=""></div>',
  directives : [LayoutItem]
})
@YourCustomAnnotation({})
export class MyCmpA {
  constructor() {

  }
}
  • Finally in the directive you can have access to the component's constructor (from your updated question I guess that's all you need to get its metadata) (You must use it inside onInit, "reference" won't exist in constructor)
@Directive({
  selector : '[layout-item]',
  properties : ['reference: parent-reference']
})
export class LayoutItem {
  constructor() {
  }

  onInit() {
    console.log(this.reference.constructor);
    Reflector.getMetadata("YourCustomAnnotation", this.reference.constructor);
  }
}

Use this plnkr to do your tests.

Eric Martinez
  • 31,277
  • 9
  • 92
  • 91
  • Hi @Eric Martinez. Thank you, you gave me some interesting ideas with this approach. Unfortunately it's not exactly what I was looking for. I made some tests with @Ancestor({self: true}) and it works if I know exactly what is the component I'm looking for. In my case it can be anything so I'm not quite there yet. Do you have some ideas? – jpsfs Aug 03 '15 at 15:30
  • I'm really sorry but I'm not able to understand exactly what do you want. Would you mind to update your question and add more details (How do you inject your directive, the metadata you want to read, is the div in the example the component's template or are they separated, etc)? I'm pretty sure that my approach is what you need (if I understood correctly what you want, and for what you told me I didn't :P). – Eric Martinez Aug 03 '15 at 16:13
  • Thank you for taking the time to look into this. I have updated the code to be a little more specific. Please let me know if you can think of anything. – jpsfs Aug 03 '15 at 16:46
  • I've updated my answer, see it that works for you now. I couldn't test the custom annotation and Reflector since I don't know how to, is that from angular2? Or an external library? (I just added them here in the answer, they are not in the plunkr) – Eric Martinez Aug 03 '15 at 18:32
  • Thank you! I'm sorry I didn't expressed myself correctly. Your first answer worked nicely!What I'm trying to avoid is declaring over and over again a property on my components just so my directive can access them. I'm trying to find a way to at least hide this in some parent class to avoid ao much repeated code. As soon as I have some working code I will get back to you! – jpsfs Aug 03 '15 at 19:07
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/85034/discussion-between-eric-martinez-and-jpsfs). – Eric Martinez Aug 03 '15 at 19:52
3

I was able to get access to a directive's host component by asking the injector for it.

@Directive({
  selector: '[with-foo]'
})
export class WithFooDirective implements OnInit {
  constructor(private myComponent: MyComponent) { }

  ngOnInit() {
    console.debug(this.myComponent.foo()) // > bar
  }
}

@Component({
  selector: 'my-component',
  template: '<div></div>'
})
export class MyComponent {
  public foo() { return 'bar' }
}

...

<my-component with-foo></my-component>
t.888
  • 3,862
  • 3
  • 25
  • 31
  • 1
    I confirmed that this works with Angular 4. Is this actually a feature? Or does this happen by accident? – Daniel Hilgarth Jun 05 '18 at 12:10
  • 1
    @DanielHilgarth, I believe it's an intentional feature of the injector, although I haven't run across its use in the angular documentation. I originally encountered it in a tutorial on constructing a tabs component: https://blog.thoughtram.io/angular/2015/04/09/developing-a-tabs-component-in-angular-2.html. This is useful for cases where you know the class of the parent component because parent and child are designed to work in concert. – t.888 Jun 05 '18 at 16:07
  • Thanks for the follow up – Daniel Hilgarth Jun 05 '18 at 16:16
2

It seems that most convenient and clean way is to use provider alias:

//ParentComponent declaration
providers: [{ provide: Parent, useExisting: forwardRef(() => ParentComponent) }]

where Parent is separate class that works as OpaqueToken and abstract class at the same type.

//directive
constructor(@Optional() @Host() parent:Parent) {}

Each component that is accessed from child directive should provide itself.

This is described in documentation: link

kemsky
  • 14,727
  • 3
  • 32
  • 51
0

This solution was linked to in the comments of one of the other answers but it was hidden at the end of quite a long discussion so I will add it here.

Import ViewContainerRef and inject it into your directive.

import { ViewContainerRef } from '@angular/core';
...
constructor(private viewContainerRef: ViewContainerRef) {}

You can then access the following private/unsupported property path to retrieve the component instance which is associated with the element that has been decorated with the directive.

this.viewContainerRef._data.componentView.component
Scott Munro
  • 13,369
  • 3
  • 74
  • 80
  • Just a disclaimer that this only works for Angular 4.x. – Steve Brush May 30 '17 at 16:24
  • Thanks Steve. I was not aware of that. We are using Angular 4 which fits with what you wrote. – Scott Munro Jul 02 '17 at 23:46
  • Accessing `_data` directly is bad practice, due to the fact that it's not specified in the official angular docs. This means that the angular devs can introduce breaking changes on the `_data ` element without notice, causing your solution to break in further releases. – pxr_64 Dec 15 '17 at 07:10
  • @user2960896 Agreed. I did mention that the property is "private/unsupported" in my answer. – Scott Munro Jan 23 '18 at 13:48
0

Not the most convenient, but reliable method. Directive:

@Input() component: any;

Component:

[component]="this"
Brackets
  • 464
  • 7
  • 12