7

I'm using Angular 2.0.1.

I have a component that can take in any other component via <ng-content> -- this works great.

The issue I run into is when I want to reference the injected component.

If I knew that <ng-content> would only ever be one component I could say: @ContentChild(MyComponent) dynamicTarget: IMyComponent; but because it could be any component (the only assumption I would make is that any injected component implements a specific interface) it become trickier.

I've also tried <ng-content #dynamicTarget'> and then referring to it by saying @ContentChild('dynamicTarget') dynamicTarget: IMyComponent; but this returns undefined.

Does anyone know how I could tell Angular 2 that this thing is an instance of a component so that I can attempt to call a function on it?

To further clarify the use case -- I have a multi-step wizard that could take in any component as content, and I want to call the validate function on the content (which again, I would assume exists on said instance)

Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
Hanna
  • 10,315
  • 11
  • 56
  • 89
  • 1
    "The best way to solve a problem is to make sure you no longer have it". If your steps are defined as components, instead of using transclusion (NgContent) you can create them manually in the wizard and pass them as configuration. For example check this Plunker (http://plnkr.co/edit/6I5e53fOzu9ywS3FzHlc). – S.Klechkovski Nov 07 '16 at 01:35
  • @S.Klechkovski My very first prototype actually used an array of components as part of a config, much like this one and I did have it working then. I guess I'm really more curious if it's possible to do with `ng-content` instead. (If for no other reason than to build out your wizard in HTML vs a config object). I suppose if that's not a possibility I would go back to a config object. – Hanna Nov 07 '16 at 03:24

3 Answers3

7

One approach could be to give the same #id to any dynamic component. I've given #thoseThings. (I think it's almost same as @Missingmanual)

PLUNKER (see console for the matches.)

@Component({
  selector: 'my-app',
  template: `
  <div [style.border]="'4px solid red'">
    I'm (g)Root.

    <child-cmp>
      <another-cmp #thoseThings></another-cmp>
    </child-cmp>
  </div>
  `,
})
export class App {
}


@Component({
  selector: 'child-cmp',
  template: `
    <div [style.border]="'4px solid black'">
        I'm Child.
      <ng-content></ng-content>
    </div>
  `,
})
export class ChildCmp {
  @ContentChildren('thoseThings') thoseThings;

  ngAfterContentInit() {
    console.log(this.thoseThings);

    this.validateAll();

    if(this.thoseThings){
     this.thoseThings.changes.subscribe(() => {
       console.log('new', this.thoseThings);
     }) 
    }
  }

  validateAll() {
    this.thoseThings.forEach((dynCmp: any) => {
      if(dynCmp.validate)
       dynCmp.validate();  // if your component has a validate function it will be called
    });
  }
}


@Component({
  selector: 'another-cmp',
  template: `
    <div [style.border]="'4px solid green'">
        I'm a Stranger, catch me if you can.
    </div>
  `,
})
export class AnOtherCmp {
}
Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
  • This is actually really cool, I hadn't considered throwing the ID on the outside like that (I had mine on the ng-content itself, which didn't work). It is slightly less than ideal in the sense that the implementer needs to throw the ID on there but it's better than the config technique because this allows multiple components on a page and reads better. So far this is the best option. Thank you Ankit. – Hanna Nov 12 '16 at 17:41
  • 1
    I was able to implement this in my code, and I actually really like the results of this so I'm going to accept this and award the bounty. Thanks again Ankit. – Hanna Nov 12 '16 at 18:34
  • I'm really glad that it helped :) I appreciate the generosity, thanks ;) – Ankit Singh Nov 14 '16 at 03:36
1
@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App, Parent, Transcluded1, Transcluded2 ],
  providers: [
    {provide: TranscludedBase, useExisting: Transcluded1, multi:true}
    {provide: TranscludedBase, useExisting: Transcluded2, multi:true}
  ],
  bootstrap: [ App ]
})

Plunker example

Plunker example

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • [Issue #8580](https://github.com/angular/angular/issues/8580) seems to be very similar (and is supposedly supported) -- the only difference is I'm using dynamic components vs hardcoded components. I've gone ahead an filed an issue for Angular just to get some better insight on this. – Hanna Nov 07 '16 at 22:31
  • Additionally, while I like the compromise of the second example, it does still require an array of components, which essentially means defining everything twice which makes things less maintainable -- but I do appreciate the examples and the alternative solutions. – Hanna Nov 07 '16 at 22:31
-1

If you know that all of your components would have validate and implement the same interface e.g. IValidate, then you can say

@ContentChild('IValidate') dynamicTarget: IValidate;
// ...
this.dynamicTarget.validate();

Angular doesn't care about exact class as long as it fits the requested interface.

Miroslav Jonas
  • 5,407
  • 1
  • 27
  • 41
  • 1
    If this were to work that would be great. But both with interfaces and abstract classes I have had no success with this technique. If you are able to get this to work (with a plunkr) then I'd be happy to accept that as the best answer. – Hanna Nov 07 '16 at 22:12