Reputation: 33
I'm using angular material design components and want to create a custom grid-list component. The component would adapt the number of grid-list-columns based on its size. The components template would look like this:
<mat-grid-list [cols]="adaptiveCols">
<ng-content></ng-content>
</mat-grid-list>
And the component will be used like this:
<my-grid>
<mat-grid-tile>tile_content</mat-grid-tile>
<mat-grid-tile>tile_content</mat-grid-tile>
<mat-grid-tile>tile_content</mat-grid-tile>
</my-grid>
So you would think this would display a <mat-grid-list>
with 3 <mat-grid-tile>
s in them, and the resulting DOM will contain those elements. But you don't see the tiles nor their content.
Adding more tiles to the grid-list and using a fixed rowHeight
doesn't affect the height
-attribute of the grid-list (from what I could figure out the grid-list calculates its height based on the number and col-/rowspan of tiles). That makes you think the grid-list doesn't "see" the <mat-grid-tile>
-children. Looking at the source code of the grid-list and setting a breakpoint on _layoutTiles()
confirmed this, in this method this._tiles
is empty.
My guess is the angular ContentChildren
-annotation (here) doesn't find the tile-children in the first code snippet because it only works on the first level of DOM, and that first level we only have the <ng-content>
.
The desired result is clear, I want to be able to use a custom grid-list component, give it some children and use its features. But for some reason, angular doesn't want me to succeed, or I still have the stuff to learn.
I'm using angular 6.1 and installed the material-components using the official docs.
Upvotes: 3
Views: 3338
Reputation: 29335
mat-grid-list uses content projection in it's own implementation and so it needs to be able to query it's projected content. Trying to add another level of content projection makes this not possible since the mat-grid-tile's are not actually being projected by the the list, but by your component.
So short answer: this isn't gona work.
There is likely a way to achieve your goal but content projection isn't the way. If you clarify your intentions a bit, I may be able to help. You likley need to use templates though.
Here's the simple case: write a wrapper around mat-grid-list that accepts an array of data as input that you can iterate over with ngFor to create the tiles
@Component({
selector: 'my-grid',
templateUrl: './my-grid.html'
})
export class MyGrid {
@Input() data: string[];
}
<mat-grid-list>
<mat-grid-tile *ngFor="let d of data">{{d}}</mat-grid-tile>
</mat-grid-list>
This provides a reusable wrapper around mat-grid-list that abstracts a bit of it away for you. however, this is no good if your grid content is more complex than a string (likely) or requires custom templates on every usage. In this case, you need a somewhat more refined approach, using templates and directives:
@Directive({
selector: 'gridTemplate'
})
export class GridTemplate {
@Input() key: string;
constructor(private elementRef: ElementRef, public template: TemplateRef<any>) {}
}
This directive will identify your custom templates then, in your grid list, you can use these templates to populate the tiles
@Component({
selector: 'my-grid',
templateUrl: './my-grid.html'
})
export class MyGrid implements AfterContentInit {
//structure the data so it can be assigned to a template by key
@Input() data: {templateKey:string, data: any}[];
//use content children to find projected templates
@ContentChildren(GridTemplate)
gridTemplates: QueryList<GridTemplate>;
gridTemplateMap: {[key:string]: TemplateRef<any>} = {};
ngAfterContentInit() {
//store queried templates in map for use in template
this.gridTemplates.forEach(t => this.gridTemplateMap[t.key] = t.template);
}
}
<ng-template #defaultGridTemp let-d="data">{{d}}</ng-template>
<mat-grid-list>
<mat-grid-tile *ngFor="let d of data">
<!-- here we use the template map to place the correct templates or use a default -->
<!-- we also feed the data property as context into the template -->
<ng-container [ngTemplateOutlet]="(gridTemplateMap[d.templateKey] || defaultGridTemp)"
[ngTemplateOutletContext]="{ data: d.data }" ></ng-container>
</mat-grid-tile>
</mat-grid-list>
Then your usage of your component is like this:
@Component({...})
export class MyComponent {
data = [
{ templateKey:'MyKey1', data: {message: 'Projected Message', number: 1} },
{ templateKey:'MyKey2', data: {whatever: 'Message', youWant: 'to place'} }
];
}
<my-grid [data]="data">
<ng-template gridTemplate key="MyKey1" let-d="data">
<div>{{d.message}}</div>
<div>{{d.number}}</div>
<div>Any other arbitrary content</div>
</ng-template>
<ng-template gridTemplate key="MyKey2" let-d="data">
<div>{{d.whatever}}</div>
<div>{{d.youWant}}</div>
<div>Any other arbitrary content</div>
</ng-template>
</my-grid>
with this approach, you can add whatever reusable logic to mat-grid-list you want inside your grid component but still maintain the flexibility of providing custom templates to your grid. The drawback is that you have to take the extra steps to structure your data to take advantage of this approach.
Upvotes: 2