Search code examples
angularangular-content-projection

How can i see if there is some sibling projected content?


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 ?


Solution

  • 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