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,
template: `
<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],
template: `
<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.
We can use the
static: true
of theViewChild
, which is set to default asfalse
. In the documentation it saysSo when the query resolves before change detection runs, the before change detection and after detection shows the same values and will get rid of this error!
By doing these changes, It just makes the template to be fetched before change detection runs, this has the advantage of the template being ready before the view is initialized, so we do not get the error, as to why the hooks are firing earlier, like said above, since static is true, the timing got changed is my understanding, but we need to look into the angular source code to give an exact answer!
Stackblitz Demo