简体   繁体   中英

does angular content projection inside *ngFor share same data? How to access project content of child component from parent?

currently I trying to project a third component in a child component which is projected inside ngFor loop (inside child), but in parent whenever I change or set some property in the projected content using index of query list (ViewChildren('#thirdComponent')) in parent all the child's projected content shows same change. Is there any proper way of doing this.

Is it due to duplicating of select property binding at the place of content projection in child component.Child's projection is done inside a accordion with one or many panels opened at a time.

@Component({
  selector: "my-app",
  template: `
    <child-comp #child>
      <ng-container selected>
        <some-other-comp #someOtherComp></some-other-comp>
      </ng-container>
    </child-comp>
  `,
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
  h = 0;
  i = 1;
  j = 2;
  k = 3;
  @ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;

  ngAfterViewInit(): void {
    this.otherCompList.toArray()[this.h].prop = this.h;
    // below will result in undefined due to QueryList of size 1 
    // this.otherCompList.toArray()[this.i].prop = this.i;
    // this.otherCompList.toArray()[this.j].prop = this.j;
    // this.otherCompList.toArray()[this.k].prop = this.k;
  }
}

@Component({
  selector: "child-comp",
  template: `
    <div *ngFor="let value of [1, 2, 3]; let i = index">
      <!-- if ngIf is removed than only the last projection is diplayed -->
      <div *ngIf="i === 0">
        <ng-content select="[selected]"> </ng-content>
      </div>
    </div>
  `,
  styleUrls: ["./app.component.css"]
})
export class ChildComponent {}

@Component({
  selector: "some-other-comp",
  template: `
    <p>{{ prop }}</p>
  `,
  styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
  prop: any;
}

Stackblitz

Utilizing *ngTemplateOutlet and let-variables

We can pass along a template into our child-component, and utilize the @Input() decorator in conjunction with *ngTemplateOutlet to directly access the property from the HTML template in the parent.

Example

First, I've defined an array in my parent component which I want to use as the basis for my loop in my outer-child component.

Parent Component

@Component({
  selector: 'parent',
  templateUrl: 'parent.component.html',
  styleUrls: ['parent.component.scss']
})
export class ParentComponent implements OnInit {

  dataItems: { title: string, description: string }[] = [{
    title: 'First Element',
    description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet, nihil!'
  }...] // remaining items truncated for brevity.

  constructor() {
  }

  ngOnInit(): void {
  }

}

This parent component then has a child component, which takes an input of the entire list of items

<child [items]="dataItems"></child>

Child-Component (fist level)

@Component({
  selector: 'child',
  templateUrl: 'child.component.html',
  styleUrls: ['child.component.scss']
})

export class ChildComponent implements OnInit {
  @Input() items!: any[];
  
  constructor() {
  }

  ngOnInit(): void {
  }
}
<ng-container *ngFor="let childItem of items">
  <projected [item]="childItem">
    <ng-template let-item>
      <h4>{{item.title}}</h4>
      <p>{{item.description}}</p>
    </ng-template>
  </projected>
</ng-container>

Projected component (sub-child)

@Component({
  selector: 'projected',
  templateUrl: 'projected.component.html',
  styleUrls: ['projected.component.scss']
})

export class ProjectedComponent implements OnInit {
  @Input() item: any;
  @ContentChild(TemplateRef) templateOutlet!: TemplateRef<any>
  constructor() {
  }

  ngOnInit(): void {
  }

}
<ng-container *ngTemplateOutlet="templateOutlet; context: {$implicit: item}"></ng-container>
<ng-content></ng-content>

How does it work

The Parent Component isn't strictly necessary in this relationship, as we aren't projecting content directly from the parent into the ProjectedComponent , I simply chose to define a list of items here to keep a hierarchy similar to your question.

The Child Component
The child component does two things:

  • Defines a *ngFor loop to loop thru some collection of elements.
  • Defines a template for how these elements should be utilized in the ProjectedComponent 's template.

In the ProjectedComponent we utilize the @ContentChild decorator to select the TemplateRef which we expect to be given via <ng-content>

This template is then put into a container using the *ngTemplateOutlet which also allows us to create a data-binding context to a local variable.

the context: {$implicit: item} tells Angular that any let-* variable defined on the template without any explicit binding should bind to the item property in our component.

Thus, we are able to reference this property in the template at the parent-component level.

Edit

Technically, the context binding is not necessary if you want to define the template directly inside of the child component, as you have a direct reference to the *ngFor template, however it becomes necessary if you want to lift the template out to the ParentComponent level to make the solution more reusable.

You are correct the reason for the bug (changing just the last element) is because when rendered you have multiple elements with the same select value.

A possible solution is to use template reference to pass the desired child component from the top level to the place where you want it to be projected.

Here is a working StackBlitz

 import { AfterViewInit, Component, Input, QueryList, ViewChildren } from "@angular/core"; @Component({ selector: "my-app", template: ` <child-comp #child [templateRef]="templateRef"> </child-comp> <ng-template #templateRef> <some-other-comp #someOtherComp></some-other-comp> </ng-template> `, styleUrls: ["./app.component.css"] }) export class AppComponent implements AfterViewInit { h = 0; i = 1; j = 2; k = 3; @ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>; ngAfterViewInit(): void { this.otherCompList.toArray()[this.h].prop = this.h; this.otherCompList.toArray()[this.i].prop = this.i; this.otherCompList.toArray()[this.j].prop = this.j; this.otherCompList.toArray()[this.k].prop = this.k; } } @Component({ selector: "child-comp", template: ` <div *ngFor="let value of [1, 2, 3, 4]; let i = index"> <,-- if ngIf is removed than only the last projection is diplayed --> <ng-container *ngTemplateOutlet="templateRef"></ng-container> </div> `: styleUrls. ["./app.component;css"] }) export class ChildComponent { @Input() templateRef: } @Component({ selector, "some-other-comp": template, ` <p>{{ prop }}</p> `: styleUrls. ["./app.component:css"] }) export class SomeOtherComponent { prop; any; }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM