Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to access the host component from a directive ? #8277

Open
frankpepermans opened this issue Apr 27, 2016 · 113 comments
Open

How to access the host component from a directive ? #8277

frankpepermans opened this issue Apr 27, 2016 · 113 comments

Comments

@frankpepermans
Copy link

@frankpepermans frankpepermans commented Apr 27, 2016

According to the change log (https://github.com/angular/angular/blob/master/CHANGELOG.md),
AppViewManager is renamed into ViewUtils and is a mere private utility service.

However, I was using AppViewManager.getComponent to obtain a Component reference from an ElementRef before, how would I do this now?

What I need to achieve is the following:

  • I have a Component with a selector (say: selector my-component)
  • A directive that can be placed upon Components (my-directive)

In HTML, it looks as following
<my-component my-directive></my-component>

From my-directive, I need to have a reference to the my-component Component instance.

If there is another (better) way of doing this, I'm happy to adapt my code

@tbosch
Copy link
Contributor

@tbosch tbosch commented Apr 27, 2016

You could just inject that component into the directive.

On Wed, Apr 27, 2016 at 4:34 AM Frank Pepermans notifications@github.com
wrote:

According to the change log (
https://github.com/angular/angular/blob/master/CHANGELOG.md),
AppViewManager is renamed into ViewUtils and is a mere private utility
service.

However, I was using AppViewManager.getComponent to obtain a Component
reference from an ElementRef before, how would I do this now?

What I need to achieve is the following:

  • I have a Component with a selector (say: selector my-component)
  • A directive that can be placed upon Components (my-directive)

In HTML, it looks as following

From my-directive, I need to have a reference to the my-component
Component instance.

If there is another (better) way of doing this, I'm happy to adapt my code


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#8277

@frankpepermans
Copy link
Author

@frankpepermans frankpepermans commented Apr 27, 2016

How would that work?

The component could be anything really, or should in that case the component resolve a provider with its own instance as value, so that the directive can inject it dynamically?

@zoechi
Copy link
Contributor

@zoechi zoechi commented Apr 27, 2016

@tbosch I think that would only work when the type is statically known by the directive but if the directive is added to different components, how would one inject it?

@frankpepermans
Copy link
Author

@frankpepermans frankpepermans commented Apr 28, 2016

@tbosch can you comment an alternative? If getting a component from an ElementRef will be dropped, let me know so I can start reworking my solution.

@bennylut
Copy link

@bennylut bennylut commented Apr 30, 2016

Any workaround yet? I have a directive that assumes that the host component will have some specific interface. Without this feature (or some workaround) I will have to develop this directive for each component that implements this interface (which is what I will do if no other option)..

@CaptainCodeman
Copy link

@CaptainCodeman CaptainCodeman commented Apr 30, 2016

If the component has to be static, doesn't it kind of defeat the whole point of directives - why wouldn't you just move whatever the directive does right into the known component?

@bennylut
Copy link

@bennylut bennylut commented Apr 30, 2016

@CaptainCodeman, The idea was that instead of repeating the implementation of the same functionality in every component that follows a specific interface, I would have a directive that add this functionality.

Here is a simplified example:
Say I have a selection-info component that is just a simple panel that display information about some selected data.
Now I have three components: list-view, tree-view and combo-box, each of these components has the following properties: selection:T, selectionChange:EventEmitter and valueToString: (T)=>string,
in the previous version I could write a simple directive track-selection that receive an instance of selection-info as input, and take care of binding all the needed properties and even supply some additional services for debugging. In the new version it of course still possible but in a less elegant way in my eyes.

This is of course a relatively simplified scenario but I hope it will be able to show the rational behind receiving the component in a directive without actually knowing it exact class but only it interface.

@CaptainCodeman
Copy link

@CaptainCodeman CaptainCodeman commented Apr 30, 2016

Yes, that is what a directive is supposed to do - add behavior to whatever component it's attached to.

If that can no longer be done then it makes them kind of pointless - the behavior could be moved to a class that is pulled into each component instead (if things have to be statically defined).

@bennylut
Copy link

@bennylut bennylut commented Apr 30, 2016

@CaptainCodeman right, for now this is what I will do. But I hope that the angular team will provide an alternative as the directive approach was much more elegant..

@frankpepermans
Copy link
Author

@frankpepermans frankpepermans commented Apr 30, 2016

This is exactly what I've been using directives for, a way to elegantly allow composition and enhance components.

In my case state management, the component must implement a certain interface, the directive asserts that the component indeed implements it and then does its thing.

I do hope this will still be possible :)

@dqweek
Copy link

@dqweek dqweek commented May 2, 2016

I have same issue described.
I have found a dirty workaround, till official fix will be provided.

This code works for me:

@Directive({
    selector: 'foo'
})
class FooDirective
{
   constructor(private _view: ViewContainerRef) {
  }
  ngOnInit() {
      let component = (<any>this._view)._element.component

     //TODO: add initialization code here
 }
}

explanation:
AppElement initialized with component instance on compilation.
AppElement is private to angular, so we can't DI it.
Fortunately ViewContainerRef has a reference to it on initialization (it is private).

it is a short run fix, till angular team provide better solution.

@zoechi
Copy link
Contributor

@zoechi zoechi commented May 2, 2016

This doesn't work for Dart though because in Dart access to private members is effectively prevented.

@jpsfs
Copy link

@jpsfs jpsfs commented May 10, 2016

Hi,

@tbosch this is a feature needed by some folks (including myself) as you can see on StackOverflow:
http://stackoverflow.com/questions/31748575/how-to-access-the-component-on-a-angular2-directive/

Best,
José

@syndicatedshannon
Copy link

@syndicatedshannon syndicatedshannon commented May 24, 2016

We attempted to use this today, but as part of a workaround for a workaround for another need. The use-case may still be relevant, so I'll describe it briefly.

Our hierarchical tree needs to Observe the result of a late-determined function, displaying resulting children. However, an Angular 2 function in a view, even one returning an Observable piped to async, never settles. To work-around, we passed a function reference from the view instead, which also required the view-containing component's context.

@giancarloa
Copy link

@giancarloa giancarloa commented Jun 16, 2016

anyone know if a supported solution to this problem is available now? I too have a similar requirement... I have a child component that can literally be used inside of any type of parent component... and the child component needs to access the parent component...

@jpsfs
Copy link

@jpsfs jpsfs commented Jul 14, 2016

@tbosch @mhevery
Any plan to include this shortly?

Or at least an indication of how to tackle this problem using another approach?

@vicb
Copy link
Contributor

@vicb vicb commented Sep 27, 2016

/cc @tbosch

@vicb vicb changed the title [beta.16] AppViewManager replacement? How to access the host component from a directive ? Oct 6, 2016
@Luke265
Copy link

@Luke265 Luke265 commented Nov 19, 2016

Any news on this? The only one @dqweek approach has been patched in 2.2.0.

@zoechi
Copy link
Contributor

@zoechi zoechi commented Nov 19, 2016

@DzmitryShylovich that doesn't seem to be related to me

@DzmitryShylovich
Copy link
Contributor

@DzmitryShylovich DzmitryShylovich commented Nov 19, 2016

yeah, nvm

@dominique-mueller
Copy link

@dominique-mueller dominique-mueller commented Nov 25, 2016

We're having the same issue here. The workaround worked so far, yet got removed with the Angualr 2.2 release. In general, I think Angular should provide a proper way for directives getting a reference to the components they're attached to. Only this way, directives and components can interact directly, thus allowing directives to enhance components (like others mentioned above).

Are there any news on this? With this issue unsolved, directives are not as powerful and thus helpful as they could be.

@ghetolay
Copy link
Contributor

@ghetolay ghetolay commented Nov 25, 2016

What's wrong about using a base class or a token on your component and use that to inject it into the directive like that https://plnkr.co/edit/xSyOseYau8YFXnsxnsMA?p=preview ?

If you want to inject a component into a directive you must certainly know something about that component, class/interface or you won't be able to interact with it.

@dominique-mueller
Copy link

@dominique-mueller dominique-mueller commented Nov 28, 2016

Thanks so much, your solution(s) work perfectly! I didn't know about the @self decorator, the Angular documentation should probably be updated to include this approach.

@10229428
Copy link

@10229428 10229428 commented May 22, 2020

@yoan-asdrubal I used your method in my project. But when I update my project to ng9, this method is no longer available. Is there anyother method to get the host component instance from a directive, in ng9?

@10229428
Copy link

@10229428 10229428 commented May 22, 2020

@RockNHawk Your method is very good, but need a base class for every component. In my scene, there is no base class for components, alse I can not modify them. Do you have some other ideas? Thanks!

@leosdibella
Copy link

@leosdibella leosdibella commented May 25, 2020

Depending on the use case, you can just invert the relationship and inject the directive into the component. E.g.

@Directive({
   selector: '[some]'
})
export class SomeDirective() {
    public someComponentFunction = () => {};

    public something() {
      //.... 
      // call something dynamically set by a component
      this.someComponentFunction();
      // ....
      // do other things maybe
    }
}

@Component({
     selector: 'example',
     template: '<div>Example</div>'
})
export class ExampleComponent implements OnInit {
    public ngOnInit() {
        if (this._someDirective) {
           this._someDirective.someComponentFunction = () => {
               // ... do whatever you gotta do
           };
        }
    }
    constructor(@Optional() private readonly _someDirective: SomeDirective) {}
}
@CorrM
Copy link

@CorrM CorrM commented Jun 22, 2020

My workaround solution Tested on Angular 9.1

    public GetComponent(): any {
        const container = this._viewContainerRef["_lContainer"][0];
        if (container instanceof HTMLElement) {
            return container;
        }

        let containerName = container.constructor.name;
        let componentName = containerName.split("_")[1];
        return container.filter((x: any) => x && x.constructor.name == componentName)[0];
    }

EDIT:
Tested in Angular 10.0.0
Work only in debug build

@stephenlautier
Copy link

@stephenlautier stephenlautier commented Oct 28, 2020

Any solution for this one yet? Even if its a bit hacky, but i dont have access of the parent (but not for debug only either)

Used to use this one for non ivy:

(this.viewContainer as any)._view.component

Basically all i need it for is to execute a function in correct context (this)

const hostComponent = (this.viewContainer as any)._view.component;
// execute is coming from input
const execFn = this.execute.bind(hostComponent);
@mlc-mlapis
Copy link
Contributor

@mlc-mlapis mlc-mlapis commented Oct 28, 2020

@stephenlautier
Copy link

@stephenlautier stephenlautier commented Oct 28, 2020

@stephenlautier

Look at: #10448 (comment)

Thats having an abstraction over 1 parent component, so its not good for my case. I need the actual view host of the component is in (not specified anywhere) e.g. if you have 5 pages (so 5 components) the directive should pick its parent depending where it is in.

For now what i've did I just pass the host via an input as well e.g. <div myDirective [myDirectiveHost]="this">, but ideally I don't need to pass it as the usage would be nicer

@CorrM
Copy link

@CorrM CorrM commented Oct 28, 2020

My workaround solution Tested on Angular 9.1, 10.2.1
Work on BOTH [debug, release] build.

private _viewRef: any;

constructor(private _viewContainerRef: ViewContainerRef, private el: ElementRef) {
        this._viewRef = this.GetComponent();
}
public GetComponent(): any {
        const container = this._viewContainerRef["_lContainer"][0];
        if (container instanceof HTMLElement) {
            return container;
        }
        return container[8];
}

Used by my lib here
https://github.com/CorrM/MyAngularUi/blob/master/MAU.Angular/projects/my-angular-ui/src/lib/mau-element.directive.ts#L20

@cbejensen
Copy link

@cbejensen cbejensen commented Nov 10, 2020

@valorkin isn't ng.getComponent meant to be used for debugging? From my understanding it won't even work in a production build.

@Totati
Copy link

@Totati Totati commented Nov 10, 2020

I simply inject the component and it works for me.

<my-component highlight></my-component>
@Directive({selector: '[highlight]'})
export class HighlightDirective {
  constructor(private component: MyComponent) {  }
}

Yes, but the problem is... what if you didn't know the type of your component? what if you wanted one of 20 different components and you don't know which it is until after you get it?

ie. different components that share some similarity (ie. all extends the same class).

Use an InjectionToken and provide your component as it. StackBlitz

@sam-s4s
Copy link

@sam-s4s sam-s4s commented Nov 10, 2020

@valorkin isn't ng.getComponent meant to be used for debugging? From my understanding it won't even work in a production build.

@cbejensen You are correct.
https://angular.io/api/core/global
"Exposes a set of functions in the global namespace which are useful for debugging the current state of your application. These functions are exposed via the global ng "namespace" variable automatically when you import from @angular/core and run your application in development mode. These functions are not exposed when the application runs in a production mode."

@valorkin
Copy link
Contributor

@valorkin valorkin commented Nov 12, 2020

I will rephrase ng.getComponent. This works in production - tested

function  _getComponent(nativeElement): any {

    const ngDevMode = true;
    const MONKEY_PATCH_KEY_NAME = '__ngContext__';
    const TVIEW = 1;
    const HEADER_OFFSET = 20;
    const HOST = 0;

    function findViaNativeElement(lView, target) {
      const tView = lView[TVIEW];
      for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
        if (unwrapRNode(lView[i]) === target) {
          return i;
        }
      }
      return -1;
    }

    /**
     * Returns `RNode`.
     * @param value wrapped value of `RNode`, `LView`, `LContainer`
     */
    function unwrapRNode(value) {
      while (Array.isArray(value)) {
        value = value[HOST];
      }
      return value;
    }

    function readPatchedData(target) {
      return target[MONKEY_PATCH_KEY_NAME] || null;
    }

    function getLContext(target) {
      const mpValue = readPatchedData(target);
      if (mpValue) {
        // only when it's an array is it considered an LView instance
        // ... otherwise it's an already constructed LContext instance
        if (Array.isArray(mpValue)) {
          const lView = mpValue;
          let nodeIndex;
          nodeIndex = findViaNativeElement(lView, target);
          if (nodeIndex === -1) {
            return null;
          }
          return {nodeIndex, lView};
        }
      }
    }

    function getComponentAtNodeIndex(nodeIndex, lView) {
      const tNode = lView[TVIEW].data[nodeIndex];
      let directiveStartIndex = tNode.directiveStart;
      return tNode.flags & 2 /* isComponentHost */ ? lView[directiveStartIndex] : null;
    }

    function loadLContext(target, throwOnNotFound = true) {
      const context = getLContext(target);
      return context;
    }

    function getComponent(element) {
      const context = loadLContext(element, false) as any;
      if (context === null) {
        return null;
      }
      if (context.component === undefined) {
        context.component = getComponentAtNodeIndex(context.nodeIndex, context.lView);
      }
      return context.component;
    }

    return getComponent(nativeElement);
  }
@AsuScholar
Copy link

@AsuScholar AsuScholar commented Nov 24, 2020

I simply inject the component and it works for me.

<my-component highlight></my-component>
@Directive({selector: '[highlight]'})
export class HighlightDirective {
  constructor(private component: MyComponent) {  }
}

Yes, but the problem is... what if you didn't know the type of your component? what if you wanted one of 20 different components and you don't know which it is until after you get it?
ie. different components that share some similarity (ie. all extends the same class).

Use an InjectionToken and provide your component as it. StackBlitz

I actually like this solution. It seems much cleaner than everything else proposed.

@cbejensen
Copy link

@cbejensen cbejensen commented Nov 24, 2020

I simply inject the component and it works for me.

<my-component highlight></my-component>
@Directive({selector: '[highlight]'})
export class HighlightDirective {
  constructor(private component: MyComponent) {  }
}

Yes, but the problem is... what if you didn't know the type of your component? what if you wanted one of 20 different components and you don't know which it is until after you get it?
ie. different components that share some similarity (ie. all extends the same class).

Use an InjectionToken and provide your component as it. StackBlitz

This seems like it would work well for many scenarios, but what if I want to use the directive on multiple child components in the same template?

@AsuScholar
Copy link

@AsuScholar AsuScholar commented Nov 24, 2020

I simply inject the component and it works for me.

<my-component highlight></my-component>
@Directive({selector: '[highlight]'})
export class HighlightDirective {
  constructor(private component: MyComponent) {  }
}

Yes, but the problem is... what if you didn't know the type of your component? what if you wanted one of 20 different components and you don't know which it is until after you get it?
ie. different components that share some similarity (ie. all extends the same class).

Use an InjectionToken and provide your component as it. StackBlitz

This seems like it would work well for many scenarios, but what if I want to use the directive on multiple child components in the same template?

@cbejensen did you check out the StackBlitz example? With the solution you should be able to use the directive on multiple child components in the same template.

@sam-s4s
Copy link

@sam-s4s sam-s4s commented Nov 24, 2020

I simply inject the component and it works for me.

<my-component highlight></my-component>
@Directive({selector: '[highlight]'})
export class HighlightDirective {
  constructor(private component: MyComponent) {  }
}

Yes, but the problem is... what if you didn't know the type of your component? what if you wanted one of 20 different components and you don't know which it is until after you get it?
ie. different components that share some similarity (ie. all extends the same class).

Use an InjectionToken and provide your component as it. StackBlitz

This seems like it would work well for many scenarios, but what if I want to use the directive on multiple child components in the same template?

Each directive will be injected with its own parent, via the token.
ie. in the stackblitz... D_COMPATIBLE_COMPONENT is a token used for both C1Component and C2Component. So if you have 2 directives in the same template, one on C1 and one on C2, they will both correctly pull their (different) parents.

@AlexandrLesiv
Copy link

@AlexandrLesiv AlexandrLesiv commented Feb 11, 2021

I find that this code works just fine in angular 11

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appAdvancedFilter]' })
export class AdvacedFilterDirective {
  constructor(host: ViewContainerRef){
    const component = getComponent(host);
    console.log(component);
  }
}

// it's unstable and may break in next angular versions
const getComponent = (ref: any) => {
  const index = ref._hostTNode.directiveStart;
  const component = ref._hostLView[index];
  return component;
};
@sam-s4s
Copy link

@sam-s4s sam-s4s commented Feb 11, 2021

I just wish the angular team would give us an easy method of doing this, that's not using an underscored variable that could break in any upgrade :P It has been 5 years now...

@sgracki
Copy link

@sgracki sgracki commented Feb 16, 2021

@sam-s4s InjectionToken approach is not a rocket science. Even code example was provided. Imho there is no need to improve this part by angular team at all

@geethapriya-mc
Copy link

@geethapriya-mc geethapriya-mc commented Feb 26, 2021

I find that this code works just fine in angular 11

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appAdvancedFilter]' })
export class AdvacedFilterDirective {
  constructor(host: ViewContainerRef){
    const component = getComponent(host);
    console.log(component);
  }
}

// it's unstable and may break in next angular versions
const getComponent = (ref: any) => {
  const index = ref._hostTNode.directiveStart;
  const component = ref._hostLView[index];
  return component;
};

I am using angular 11. This solution worked for me to get the component. However the changes done to component in the directive are not reflected in host component. is there a way to get this working?

@sam-s4s
Copy link

@sam-s4s sam-s4s commented Feb 28, 2021

@sam-s4s InjectionToken approach is not a rocket science. Even code example was provided. Imho there is no need to improve this part by angular team at all

I agree that it's not difficult to implement a token-based solution, but I'd argue these 2 points:

a) It adds unnecessary (and messy) lines of code.
b) It is not intuitive. Anyone seeking the hosting component of a directive (and we know from this thread this is not rare) will always naturally try to find a solution involving obtaining a reference to the component in a more direct manner. This means that many people will end up using the _ properties that are not meant to be public.

The point is - it's there, it shouldn't be hard to make it a permanent, public reference. Easy fix that would make a lot of people happy, and reduce danger. I'm not seeing a down-side.

@zoechi
Copy link
Contributor

@zoechi zoechi commented Mar 1, 2021

I think the token based approach is completely against what this issue is about. If you know what the host is, then there is no problem. If you don't know and don't want to need to know statically, then there is no proper way of accomplishing the task, but there should be.

@pillsilly
Copy link

@pillsilly pillsilly commented Apr 29, 2021

I find that this code works just fine in angular 11

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appAdvancedFilter]' })
export class AdvacedFilterDirective {
  constructor(host: ViewContainerRef){
    const component = getComponent(host);
    console.log(component);
  }
}

// it's unstable and may break in next angular versions
const getComponent = (ref: any) => {
  const index = ref._hostTNode.directiveStart;
  const component = ref._hostLView[index];
  return component;
};

I am using angular 11. This solution worked for me to get the component. However the changes done to component in the directive are not reflected in host component. is there a way to get this working?

My way is to proxy the ngOnchanged hook,
If it's not definded you need to make one on the components constructor prototype

but yes if there's official API to achieve then that would be nice.

@Pentadome
Copy link

@Pentadome Pentadome commented May 12, 2021

Any news?

@amninderchahal
Copy link

@amninderchahal amninderchahal commented May 14, 2021

My workaround was to pass the template ref of component as @input to the directive. It worked well in my case which was to debounce events. Type safety is still missing in this case but it works fine if used correctly.

@Directive({selector: '[debounceEvent]'})
export class DebounceEventDirective implements OnInit {
  @Input() componentRef: any;
  ...
  constructor(private el: ElementRef, private renderer: Renderer2) {  }

  ngOnInit(){
    if (this.componentRef != null) {
        // Subscribe to event emitter in component
        this.componentRef[this.eventName].subscribe();
    } else {
       // Add DOM event bindings using Renderer2
      this.renderer.listen(this.el.nativeElement, this.eventName, (e) => {});
    }
 }
}

Usage in view:

<!-- With native elements -->
<button debounceEvent [eventName]="'click'" ... ></button> 

<!-- With components -->
<app-sidebar #appSidebarRef  debounceEvent [componentRef]="appSidebarRef  " [eventName]="'sidebarOpened'"  .... >
@pillsilly
Copy link

@pillsilly pillsilly commented May 20, 2021

<app-sidebar [debounceEvent]="{component: appSidebarRef,eventName: 'sidebarOpened'}"  .... >

would upper form of usage work? @amninderchahal

@amninderchahal
Copy link

@amninderchahal amninderchahal commented May 20, 2021

@pillsilly Yes it should work.

@badrbilal
Copy link

@badrbilal badrbilal commented May 31, 2021

Until a permanent solution is available, a workaround could be to pass reference of the hosting component to the directive:

image

image

image

of course, this is just a temporary solution which requires binding on each element and a permanent solution will be appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet