I am trying to write a bunch of component that would encapsulate layout logic similar to Bootstrap's grid layout system. The list of items is dynamic, and the items are not homogeneous i.e. it can be a mix of string nodes, components or just a bunch of native HTML elements.
So the desired interface for using this component is as follows:
<app-layout [columns]="2">
<app-layout-item *ngFor="let item of dynamicItems">
{{ item }}
</app-layout-item>
<app-layout-item>
Some text
</app-layout-item>
<app-layout-item>
<button></button>
</app-layout-item>
</app-layout>
Now I implemented the components:
@Component({
selector: 'app-layout-item',
standalone: true,
templateUrl: `
<ng-template>
<ng-content></ng-content>
</ng-template>
`,
styleUrl: './layout-item.component.css',
})
export class LayoutItemComponent extends LifecycleLogger {
@ViewChild(TemplateRef) public template!: TemplateRef<void>;
}
function chunk<T>(array: T[], size: number): T[][] {
let result = [];
for (let i = 0; i < Math.ceil(array?.length / size); i++) {
result.push(array.slice(i * size, i * size + size));
}
return result;
}
@Component({
selector: 'app-layout',
standalone: true,
imports: [CommonModule],
templateUrl: `
<div *ngFor="let row of rows" class="row">
<div *ngFor="let item of row" class="col">
<ng-template [ngTemplateOutlet]="item.template"></ng-template>
</div>
</div>
`,
styleUrl: './layout.component.css',
})
export class LayoutComponent {
@Input({ required: true }) columns!: number;
@ContentChildren(LayoutItemComponent)
items!: QueryList<LayoutItemComponent>;
rows!: LayoutItemComponent[][];
ngAfterContentInit() {
this.rows = chunk(this.items?.toArray().filter(Boolean), this.columns);
}
}
It renders everything, but I'm getting ExpressionChangedAfterItHasBeenCheckedError
if there is an item in projected content with no structural directive applied on. I checked the lifecycle hooks order and found out that applying a structural directive makes the component ngOnViewInit
fire before ngOnViewInit
, but for those items not having a structural directive ngOnViewInit
fires later so their template is updated after app-layout
renders causing the error.
Here is repl.
So the question is: how does one add a wrapping element around groups of projected items that may or may not have a structural directive applied?
- I defenitely could make such a layout component with pure css, so I would not need to fit these row divs so that
app-layout
template would be as simple as<ng-content></ng-content>
. The problem with this approach is the fact that acctual css styles for this layout do not belong to the app but loaded at runtime and I have to use them. - I played around with
app-layout
@ContentChildren
, tried to get TemplateRef directly but this returns an empty list that never updates, and I don't see a big difference to overall situation. - I could apply
*ngIf="true"
to every "static" projected item but this seems weird at least. - I could turn
app-layout-item
into a structural directive so that I ensure every item has one, and the directive would just callcreateEmbeddedView
unconditionally. This feels as a workaround that relies on angular handling structural directives. - I could do direct DOM manipulations, but it increases a chanse to shoot a leg and I don't really want to do this.