【问题标题】:Angular: Using @ContentChildren to get children inside another componentAngular:使用@ContentChildren 让孩子进入另一个组件
【发布时间】:2019-03-10 05:43:45
【问题描述】:

我在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>

...&lt;mpx-component-A&gt; 是另一个在内部使用自己的 mpx-carousel 的组件。我仍然希望这里的父轮播能够检测到 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() 语句和显示预期输出与实际输出的 cmets。 (注意:在现实世界中,轮播会每隔几秒就旋转一次它的项目。但为了简单起见,我在这里删除了所有这些)。

【问题讨论】:

    标签: angular


    【解决方案1】:

    如果不是让 &lt;div&gt;s 封装您的 &lt;mpx-carousel&gt; 组件,而是将它们直接放在另一个内部,就像这样,它可以工作

    <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>
    

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

    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);
        });
    }
    

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

    【讨论】:

      【解决方案2】:

      这是一个尽管如此,就我所做的研究而言,没有自动神奇的方法来查询在另一个组件内的所有嵌套类型的组件。

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

      引用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中概念的基本实现

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

      【讨论】:

      • 这已经是很久以前的事了,但我相信我最终做了非常像这样的事情。
      【解决方案3】:

      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>
      

      Hello 组件

        @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"
      }
      

      【讨论】:

      • 我刚刚在原始问题中添加了StackBlitz link 以使事情更清楚。我无法在“OtherComponent”中创建此功能。该功能需要在 CarouselComponent 本身中自包含。我正在制作一个可重用的 Carousel,它需要能够识别其所有后代 Carousel,无论它们是直接子代、嵌套更深的后代,甚至是埋在另一个组件的实现中。 StackBlitz 给出了一个更好的例子。再次感谢您查看此内容!
      【解决方案4】:

      对不起,我无法想象你想要达到什么目的,但是

      如果你有类似的组件

      <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

      【讨论】:

      • 谢谢,这与我的情况类似,除了一件事。您将三个子 div 嵌套在 &lt;other&gt;...&lt;/other&gt; 中。想象一下,这些子 div 是在OtherComponent 的模板中定义的。因此它们是 OtherComponents 视图/模板的一部分,而不是其投影内容的一部分。我会尽快修改我的问题,以使这一点更清楚。
      • 我想你的孩子可以有他自己的“孩子”,你可以通过输入分享这个孩子。添加新答案
      猜你喜欢
      • 1970-01-01
      • 2020-09-12
      • 2016-11-02
      • 2020-02-16
      • 2016-02-19
      • 1970-01-01
      • 2019-04-06
      • 2020-07-24
      • 2018-08-23
      相关资源
      最近更新 更多