角度:使用@ContentChildren将子级包含在另一个组件中

时间:2018-10-04 18:53:30

标签: angular

我已经在Angular 6中创建了一个自定义轮播组件。简单用法如下:

<mpx-carousel>
  <div *mpxCarouselItem>Slide 1</div>
  <div *mpxCarouselItem>Slide 2</div>
  <div *mpxCarouselItem>Slide 3</div>   
</mpx-carousel>

但是它也支持嵌套,就像这样:

<mpx-carousel>
  <div *mpxCarouselItem>Slide 1</div>
  <div *mpxCarouselItem>
    <mpx-carousel>
      <div *mpxCarouselItem>Sub-slide A</div>
      <div *mpxCarouselItem>Sub-slide B</div>
    </mpx-carousel>
  </div>
  <div *mpxCarouselItem>Slide 3</div>   
</mpx-carousel>

在父CarouselComponent代码中,我想确定是否有子CarouselComponents,并访问它们的属性。所以我用@ContentChildren:

@ContentChildren(CarouselComponent, { descendants: true }) 
childCarousels: QueryList<CarouselComponent>;

ngAfterContentInit() {
    console.log(this.childCarousels.length); // 1, BUT it's a reference to itself, not the child
}

在父级轮播的ngAfterContentInit中,我看到@ContentChildren找到了1个子级CarouselComponent,这看起来不错。但是仔细检查发现,它实际上是找到了这个父轮播本身,而不是它的孩子。为了真正找到儿童轮播,我必须订阅childCarousel.changes:

@ContentChildren(CarouselComponent, { descendants: true }) 
childCarousels: QueryList<CarouselComponent>;

ngAfterContentInit() {
    console.log(this.childCarousels.length); // 1, BUT it's a reference to itself, not the child

    this.childCarousels.changes.subscribe(() => {
        console.log(this.childCarousels.length); // 2, now it includes itself and its child
    });
}

因此@ContentChildren包含父项本身有点奇怪,并且在检测到子项之前必须等待changes事件,这有点奇怪,但这是一个很容易的解决方法。

真正的麻烦始于在另一个组件内找到儿童轮播。 CarouselComponent是项目中许多其他组件所使用的共享组件,所以我经常可以有这种用法...

<!-- parent carousel -->
<mpx-carousel>
  <mpx-component-A></mpx-component-A>
  <mpx-component-B></mpx-component-B>
</mpx-carousel>

...其中<mpx-component-A>是另一个内部使用其自身的mpx轮播的组件。我仍然希望此处的父轮播能够检测mpx-component-A视图内使用的轮播。但是,当我使用上述技术时,@ ContentChildren找不到在ComponentA中定义的CarouselComponent实例。在ngAfterContentInit中,childCarousels再次仅包含一个项目,这是对父项本身的引用。在这种情况下,childCarousels.changes甚至从不开火,因此我们也不会在那儿埋葬儿童旋转木马。

@ContentChildren(CarouselComponent, { descendants: true }) 
childCarousels: QueryList<CarouselComponent>;

ngAfterContentInit() {
    console.log(this.childCarousels.length); // 1, BUT it's a reference to itself, not the child

    // The changes event never even fires
    this.childCarousels.changes.subscribe(() => {
        console.log(this.childCarousels.length); // N/A
    });
}

如果有帮助,我可以提供CarouselComponentCarouselItemDirective的完整代码。但是我的问题是更笼统的:父组件是否有任何方法可以引用包含在另一个组件(ComponentA)中的 中的某种类型的后代组件(CarouselComponent)?

我可能通过使用@ContentChildren完全将错误的树吠叫,所以我愿意接受完全不同的方法。

谢谢!

编辑: Here is a StackBlitz可以使我更清楚地知道要做什么。注意carousel.component.ts中的console.log()语句,以及注释显示了预期输出与实际输出。 (请注意:在现实世界中,轮播会每隔几秒钟对它的项目进行动画处理。但是为简单起见,我在这里删除了所有内容。)

4 个答案:

答案 0 :(得分:1)

这是一个虽然,据我的研究,没有自动神奇的方法来查询另一个组件内的所有嵌套类型的组件。

<块引用>

内容儿童:
不检索其他组件模板中的元素或指令,因为组件的模板对于其祖先来说始终是一个黑盒子。

引用自 Angular docs

过去我有类似的用例,我的解决方案是为嵌套的所有组件部分创建一个通用接口

export interface CommonQueryInterface {
  commonField: QueryList<any>
}

因此该逻辑位于最顶层(最高父组件)中,以具有遍历所有内部具有字段 commonField 的子组件的逻辑。但我认为这种方法与您想要的结果不同。

因此,在我看来,为了访问任何轮播中的所有轮播组件,可能需要创建一个轮播服务来管理这些轮播元素

该服务可能如下所示:

@Injectable({ providedIn: "root" })
export class CarouselService {
  carousells = {};

  addCarousel(c: CarouselComponent) {
    if (!this.carousells[c.groupName]) {
      this.carousells[c.groupName] = [];
    }
    this.carousells[c.groupName].push(c);
  }

  getCarousels() {
    return this.carousells;
  }

  clear(c: CarouselComponent) {
    if (c.topLevel) {
      delete this.carousells[c.groupName];
    }
  }
}

我的解决方案建议在轮播组件中将轮播元素本身添加到服务中

export class CarouselComponent implements AfterContentInit, OnDestroy {
  @ContentChildren(CarouselItemDirective) items: QueryList<
    CarouselItemDirective
  >;
  @ContentChildren(CarouselComponent, { descendants: true })
  childCarousels: QueryList<CarouselComponent>;
  @Input() name;
  @Input() groupName;
  @Input() topLevel = false;

  @ViewChildren(CarouselComponent) childCarousels2: QueryList<
    CarouselComponent
  >;

  constructor(private cs: CarouselService) {}

  getActiveCarousels() {
    return this.cs;
  }

  ngAfterContentInit() {
    if (this.name) {
      this.cs.addCarousel(this);
    }
  }

  ngOnDestroy(){
      this.cs.clear(this)
  }

  queryCarousells() {
    return this.cs.getCarousels();
  }
}

这样做后,您将能够随时访问活动的(屏幕上的)轮播。这种方法的缺点是您需要在 name @Input 旁边多一个 groupName @Input。这在我的实现中是需要的,以便我们可以在同一视图中区分不同的轮播组件。

这里是 StackBlitz

中概念的基本实现

这种实现的好处是,通过访问所有轮播,您可以在轮播组件本身内添加自定义轮播相关逻辑,并且它将被锁定,远离组件的使用者。 例如,这种逻辑可能是重新启动单个轮播组内的所有轮播。

答案 1 :(得分:0)

对不起,我无法想象您要实现什么,但是

如果您有类似的组件

<hello id="myId" name="{{ name }}">
  <div #my></div>
  <div #my></div>
  <other>
      <div #my></div>
      <div #my></div>
      <div #my></div>
  </other>
</hello>

其他组件类似

@Component({
  selector: 'other',
  template: `
  <ng-content></ng-content>`
})
export class OtherComponent{}

Hello组件

@Component({
  selector: 'hello',
  template: `<h1>Hello {{name}}!</h1>
  <ng-content></ng-content>`
})
export class HelloComponent implements OnInit,AfterViewInit {
  @Input() name: string;
  @ContentChildren('my') divs:QueryList<any>
  constructor(private el:ElementRef){}
  ngOnInit()
  {
    console.log(this.el.nativeElement.getAttribute('id'));
  }
  ngAfterViewInit()
  {
        console.log(this.divs.length)
  }
}

您看到有5个div称为“ my”,而hello的“ id”为myId,请参见https://stackblitz.com/edit/angular-yrcknx?file=src%2Fapp%2Fapp.component.html

答案 2 :(得分:0)

app.componet

<!--see that hello has a new @Input
    Use a "reference variable"
-->
<hello id="myId" name="{{ name }}" [component]="other">
  <div #my></div>
  <div #my></div>
  <other #other>
  </other>
</hello>
<p>

你好组件

  @Input() name: string;
  @Input() component: any;  //<---component, really is "other"
  @ContentChildren('my') divs:QueryList<any>
  constructor(private el:ElementRef){}
  ngOnInit()
  {
    console.log(this.el.nativeElement.getAttribute('id'));
  }
  ngAfterViewInit()
  {
        console.log(this.divs.length);
        console.log(this.component.divs.length)

  }

其他组件

@Component({
  selector: 'other',
  template: `
  <div>
  <div #my></div>
      <div #my></div>
      <div #my></div>
  </div>`
})
export class OtherComponent{
  @Input() name: string;
  //I use ViewChildren, not ContentChildren because is not in a <ng-content>
  @ViewChildren('my') divs:QueryList<any>  //<--has his own "divs"
}

答案 3 :(得分:0)

如果不是让 <div> 封装您的 <mpx-carousel> 组件,而是将它们直接放在另一个组件中,就像这样,它会起作用

<mpx-carousel name="Parent Carousel">
  <div *mpxCarouselItem>Item 1</div>
  <div *mpxCarouselItem>Item 2</div>

  <mpx-carousel name="2nd level Carousel">
    <div *mpxCarouselItem>Sub-item A</div>
    <div *mpxCarouselItem>Sub-item B</div>
    <mpx-carousel name="3rd level Carousel">
      <div *mpxCarouselItem>Sub-item A</div>
      <div *mpxCarouselItem>Sub-item B</div>
    </mpx-carousel>
  </mpx-carousel>
  <mpx-carousel name="Brother level Carousel">
  </mpx-carousel>  
</mpx-carousel>

使用这个辅助方法向我展示了预期的信息

enter image description here

ngAfterContentInit() {
    //.... existing code 
    this.showArrayObjects('childCarousels', this.childCarousels);
    //.... existing code 
}

public showArrayObjects(msg: string, array: any) {
    console.log(`${msg} - length:${array.length}..`);
    array.forEach(cc => {
      console.log(cc);
    });
}

我已经分叉了你的 stackblitz https://stackblitz.com/edit/angular-s3kqep?file=src%2Fapp%2Fapp.component.html