1

I am projecting two components.

<app-form-field>
   <component-one/>
   <component-two/>
</app-form-field>

So if i want to know for example if component one is projected inside app-form-field i will do:

  @ContentChild(ComponentOne) componentOne;

ngAfterContentInit() {
   if(this.componentOne) {
      alert('it is projected');
   } else {
      alert('it is NOT projected');
   }
}

but i need to check inside my component-two if for example component-one is projected

i need to check somehow if component-two has sibling content projection - component-one.

How can i do that in angular ?

peco123
  • 91
  • 1
  • 9

2 Answers2

1

It is possible to check if there is a sibling-component projected, but the way to go might be hacky.

First of all we need the component that projects the "child"-components.

ParentComponent

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css'],
  providers: [ParentService]
})
export class ParentComponent implements AfterContentInit {
  @ContentChildren(SelectorDirective) public refs: QueryList<SelectorDirective>;
  constructor(private p: ParentService) {}
  ngAfterContentInit(): void {
    this.p.selectorDirectives.next(this.refs.toArray());
    this.refs.changes.subscribe(x => {
      this.p.selectorDirectives.next(this.refs.toArray());
    });
  }

  ngOnInit() {}
}

Note:

  • We inject a Service ParentService on component level, so child and projected components can receive this instance.

  • For components of differents types we need a uniform Selector (SelectorDirective) to pass to @ContentChildren

ParentService:

@Injectable()
export class ParentService {
  selectorDirectives: BehaviorSubject<
    SelectorDirective[]
  > = new BehaviorSubject([]);

  constructor() {}
}

SelectorDirective:

@Directive({
  selector: '[appSelector]'
})
export class SelectorDirective {
  constructor(public componentRef: Basecomp) {}
}

Note:

  • In the constructor we inject the Basecomp type, so each element this directive is attached to, need to provide this type.

Basecomp

export class Basecomp {}

Note:

  • This is only used as injection token

Now we need to provide the intances of our child-components for our uniform token:

Comp1Component

@Component({
  selector: 'app-comp1',
  templateUrl: './comp1.component.html',
  styleUrls: ['./comp1.component.css'],
  providers: [
    { provide: Basecomp, useExisting: forwardRef(() => Comp1Component) }
  ]
})
export class Comp1Component implements OnInit {
  constructor() {}

  ngOnInit() {}
}

Note:

  • We provide the instance of this component also with the Basecomp-Token. If the selectorDirective is attached to a component which provides this token we can access the component-instance inside of the directive.

Now look back at ngAfterContentInit of ParentComponent:

  • We select all ContentChildren which have the SelectorDirective
  • We take this Elements an emit them on the Subject in our ParentService
  • Also we look for changes of the QueryList, if changes occour we emit them on the Subject in our ParentService

No we can inject ParentService in comp2 and have access to all siblings:

Comp2Component

@Component({
  selector: 'app-comp2',
  templateUrl: './comp2.component.html',
  styleUrls: ['./comp2.component.css'],
  providers: [
    { provide: Basecomp, useExisting: forwardRef(() => Comp2Component) }
  ]
})
export class Comp2Component implements OnInit {
  constructor(private p: ParentService) {}
  c1 = false;
  ngOnInit() {
    this.p.selectorDirectives.pipe().subscribe(x => {
      this.c1 = !!x.find(a => {
        return a.componentRef instanceof Comp1Component;
      });
    });
  }
}

Working Example: https://stackblitz.com/edit/angular-ivy-t3qes6?file=src/app/comp2/comp2.component.ts

enno.void
  • 6,242
  • 4
  • 25
  • 42
  • Thank you for your answer. Btw what is the meaning of - providers: [ { provide: Basecomp, useExisting: forwardRef(() => Comp2Component) } ] – peco123 Jul 28 '21 at 09:06
  • It means we provide the instance of the component inside the Elementinjector which is created for this element. This is necessary because we want to access components of different types inside the selectorDirective. This works because components and directives on the same element share one Elementinjector. You can read more about this here: https://angular.io/guide/hierarchical-dependency-injection. For `forwardRef()` see https://stackoverflow.com/questions/50894571/what-does-do-forwardref-in-angular – enno.void Jul 28 '21 at 09:18
  • Thank you very much Enno. And also - Basecomp - this should be directive or just one simple class without directive annotation ? – peco123 Jul 28 '21 at 10:01
  • Because when i try to re-implment your example i get - main.ts:12 NullInjectorError: R3InjectorError(AppModule)[Basecomp -> Basecomp -> Basecomp] error – peco123 Jul 28 '21 at 10:02
  • Inside the selectorDirective we are injection the baseComp - and that gives error because the baseComp is not in the providers array in the app.module.ts file. I watched in your example - on stackblitz the baseCompo is not in the providers array in the app.module.ts file. So how it works by you ? – peco123 Jul 28 '21 at 10:07
  • which angular version do you use? – enno.void Jul 28 '21 at 10:40
  • I am using - 10.2.7 – peco123 Jul 28 '21 at 11:17
  • Im not sure why its not working on your side, maybe adding `@Injectable()` to `Basecomp` and add `Basecomp` to providers array does the trick... – enno.void Jul 28 '21 at 11:23
  • Thank you Enno.This is a whole new concept for me. It opened a lot of unlearned things which is good. But i can't find a way - why when i project component-one in the parent component - if it is sorrunded by div - then i get baseComp in the refs instead ComponentOne instance - this is if i have
    – peco123 Jul 28 '21 at 11:47
  • You have to use `{ descendants: true }` in `@ContentChildren` to also select nested elements in your projection. It updated the example: https://stackblitz.com/edit/angular-ivy-t3qes6?file=src/app/app.component.html – enno.void Jul 28 '21 at 12:10
0

A simple solution would be to have the parent component introduce component-one to component-two explicitly:

So in your app-form-field like you write something along the lines of:

@ContentChild(ComponentOne) componentOne;
@ContentChild(ComponentTwo) componentTwo;

ngAfterContentInit() {
   this.componentTwo.componentOne = this.componentOne;
}

And then you can react to that in component-two.

I know this creates a tight coupling between app-form-field component-one and component-two but it looks to me as if they are quite coupled anyway, so this way is ok.

Aviad P.
  • 32,036
  • 14
  • 103
  • 124
  • I don't understand what is the idea behing this ? When i initialize property value in componentTwo equal to componentOne - how can i reach to that property in componentTwo ? – peco123 Jul 28 '21 at 08:58
  • Define it in componentTwo (I don't show that in my answer) - and assign to it from the parent - like I do show. – Aviad P. Jul 28 '21 at 08:59