6

I'm pretty new in angular2 and I'm trying to make a small angular component called "grid" that simply rearranges its content using transclusion.

Its template

grid component template (grid.component.ts)

<div class="grid">
  <div class="row">
    <div class="col-xs-4">
      <ng-content select="[grid-item-index=0]"></ng-content>
    </div>
    <div class="col-xs-4">
      <ng-content select="[grid-item-index=1]"></ng-content>
    </div>
    <div class="col-xs-4">
      <ng-content select="[grid-item-index=2]"></ng-content>
    </div>
  </div>
  <div class="row">
    <div class="col-xs-4">
      <ng-content select="[grid-item-index=3]"></ng-content>
    </div>
    <div class="col-xs-4">
      <ng-content select="[grid-item-index=4]"></ng-content>
    </div>
    <div class="col-xs-4">
      <ng-content select="[grid-item-index=5]"></ng-content>
    </div>
  </div>
</div>

And this is part of the parent component that uses it.

parent template

<grid>
  <div *ngFor="let item of items; index as i" [attr.grid-item-index]="i">
    <span>{{item}}</span>
  </div>
</grid>

Here's a Plunker.

But the result displays no content. But using...

<grid>
  <div grid-item-index="0">item 0</div>
  <div grid-item-index="1">item 1</div>
  <div grid-item-index="2">item 2</div>
  <div grid-item-index="3">item 3</div>
  <div grid-item-index="4">item 4</div>
  <div grid-item-index="5">item 5</div>
</grid>

it works fine and the result was as I expected.

A Plunker of this last working case.

Can achieve this result using a ngfor or similar.

I've tried using nth-child css pseudo class to avoid using the index but it doesn't work too.

UPDATE

I've made some progress based on @yurzui (Thanks!!) answer. It allows to map content with an grid-item-index value to the view container with the same grid-item-index value.

parent.component.html

<grid>
    <ng-template *ngFor="let item of items; let i=index" 
    [grid-item-index]="(items.length-1)-i">
        <span >{{item}}</span>
    </ng-template> 
</grid>

grid-item-index directive

@Directive({
  selector: '[grid-item-index]'
})
export class GridItemIndexDirective {
  @Input('grid-item-index') index: any;

  constructor(public vcRef: ViewContainerRef, public tRef: TemplateRef) {}
}

grid.component.ts

@ContentChildren(GridItemIndexDirective) sources: QueryList<GridItemIndexDirective>;
  @ViewChildren(GridItemIndexDirective) containers: QueryList<GridItemIndexDirective>;

  constructor(
    private cdRef:ChangeDetectorRef
  ) {}

  ngAfterViewInit() {
    const len = this.sources.length;
    for (var i = 0; i < len; i++) {
      const destinationContainer = this.containers.find(x => x.index == i);
      const source = this.sources.find(x => x.index == i);
      if (destinationContainer) {
        destinationContainer.vcRef.createEmbeddedView(source.tRef);
        this.cdRef.detectChanges(); // this solves ExpressionChangedAfterItHasBeenCheckedError
      }
    }
  }

Check this Plunker

Jman
  • 63
  • 7
  • 1
    Is your data received asynchronously ? – Standaa - Remember Monica Jun 15 '17 at 10:27
  • No, it's an array defined in the parent component. – Jman Jun 15 '17 at 10:40
  • Could you provide a [Plunker](https://plnkr.co) of your problem ? It's difficult to see what could cause this behaviour. – Standaa - Remember Monica Jun 15 '17 at 12:17
  • Added a Plunker to the description. One for the not working case with ngFor and another for the working case without ngfor. – Jman Jun 15 '17 at 13:05
  • My guess is that the structure of your grid component is not being recognized by *ngFor, the component being static. – Standaa - Remember Monica Jun 15 '17 at 14:56
  • As I understand the grid component must transclude the result of ngFor. And this result is the same as the second case (the one without the ngFor). I don't know what's wrong and whether there is any alternative. – Jman Jun 15 '17 at 15:57
  • 2
    Option 1 https://plnkr.co/edit/5YZpGSwhC5epCv6gWhjZ?p=preview Since `template="` is deprecated then https://plnkr.co/edit/GzaFRu2T8vKqyPdEkBjv?p=preview – yurzui Jun 15 '17 at 16:46
  • 2
    Option2 https://plnkr.co/edit/u2Jin4DA12t4LhtsRQv7?p=preview – yurzui Jun 15 '17 at 16:51
  • @yurzui That works pretty nicely!! Thanks a lot!. Can a selector be added to determine wich content is added to each ng-template? – Jman Jun 16 '17 at 07:44
  • By the way. If you post your solution as an anwser i can accept it – Jman Jun 16 '17 at 07:59
  • @Jman Which plunker did you mean when were asking about selector? – yurzui Jun 16 '17 at 15:21
  • @yurzui I'm talking about [Option2](https://plnkr.co/edit/u2Jin4DA12t4LhtsRQv7?p=preview). I think is the best option. When i ask about a selector i mean add a selector that can be used to pick the exact content to add to any ng-template (like grid-item-index in the description). Thanks – Jman Jun 16 '17 at 18:30

1 Answers1

3

You can use ngTemplateOutlet to achieve this but there is another approach that uses low-level API:

parent.component.html

<grid>
  <div *ngFor="let item of items">
    <span>{{item}}</span>
  </div>
</grid>

Additional directive that will help us to recognize destination index.

@Directive({
  selector: '[grid-item-index]'
})
export class GridItemIndexDirective {
  @Input('grid-item-index') index: any;

  constructor(public vcRef: ViewContainerRef) {}
}

grid.component.html

<div class="grid">
  <div class="row">
    <div class="col-xs-4">
      <ng-template grid-item-index="1"></ng-template>
    </div>
    <div class="col-xs-4">
      <ng-template grid-item-index="2"></ng-template>
    </div>
    <div class="col-xs-4">
      <ng-template grid-item-index="3"></ng-template>
    </div>
  </div>
  <div class="row">
    <div class="col-xs-4">
      <ng-template grid-item-index="4"></ng-template>
    </div>
    <div class="col-xs-4">
      <ng-template grid-item-index="5"></ng-template>
    </div>
    <div class="col-xs-4">
      <ng-template grid-item-index="6"></ng-template>
    </div>
  </div>
</div>

grid.component.ts

@ContentChild(TemplateRef, { read: ViewContainerRef }) vcRef: ViewContainerRef;
@ViewChildren(GridItemIndexDirective) containers: QueryList<GridItemIndexDirective>;

ngAfterViewInit() {
  const len = this.vcRef.length;
  for( var i = 1; i <= len; i++) {
    const destinationContainer = this.containers.find(x => x.index == i);
    if(destinationContainer) {
      const view = this.vcRef.detach(0);
      destinationContainer.vcRef.insert(view);
    }
  }
}

Plunker Example

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Would it be possible to add the directive (item-grid-index) to the content in order to map its content to the same item-grid-index in the view? This way you can alter the order they will be added and even discard content not having the directive. – Jman Jun 16 '17 at 19:46
  • Do you mean like in your origin question `
    `?
    – yurzui Jun 16 '17 at 19:48
  • You might also be interested in this link https://stackoverflow.com/questions/44595942/how-to-use-a-child-template-inside-a-custom-table-component-to-display-each-row He is building table as well. – yurzui Jun 16 '17 at 19:50
  • Here is my table example https://github.com/alexzuza/angular2-typescript-systemjs/tree/lection/src/table – yurzui Jun 16 '17 at 19:51
  • Yes, that was what i mean. Matching the content item with a grid-item-index value to the view item with the same grid-item-index value. I'll take a look to the question mentioned. Thanks. – Jman Jun 16 '17 at 19:59
  • Then i think better way would be using ngTemplateOutlet – yurzui Jun 16 '17 at 20:01
  • The downside is that you need always to use an array. With the other method the content can come from a ngFor or you can add it directly so is more flexible. – Jman Jun 16 '17 at 20:07
  • Anyway this last feature is a nice to have – Jman Jun 16 '17 at 20:15
  • This plunker should work for both cases https://plnkr.co/edit/OwTevzDOTBoW12EW67to?p=preview – yurzui Jun 16 '17 at 20:31
  • The problem with that last one is that it ignores the grid-item-index when usign a ngFor. I've made some progress based on one of your proposals. I think it works pretty well (you can even reverse the items order). Check this [plunker](https://plnkr.co/edit/9BXspWxbQPN90Sk2q5p0?p=preview). Any improvement is welcome. Thanks – Jman Jun 16 '17 at 22:30