1

I am trying to build Info board. Till the layout was static everything was running fine but when I tried to make the layout dynamic (user can define it) the ngOnInit wasn't called in the created components.

I have two main components one where user can select for what building he want's to see the board for (SelectBoardComponent) and second that contains the board itself (BoardComponent). When I navigate from the SelectBoardComponent to the BoardComponent everything works. But when I reload or directly access the path for BoardComponent whole Angular cycle (OnInit....) is not triggered in the dynamically added childrens.

I have reproduced this issue on stackblitz

Code

To be able to find out what component I want to create I have created a "Component registry" where I register every component that could be rendered dynamically.

import {Type} from '@angular/core';

type ComponentClass = Type<any>;

const REGISTRY = new Map<string, ComponentClass>();

export const getTypeFor = (name: string): ComponentClass => {
  return REGISTRY.get(name);
};
export const Register = () => {
  return (cls: ComponentClass) => {
    REGISTRY.set(cls.name, cls);
  };
};

So I register InfoCardComponent one of the components I want to render

import { Component, OnInit } from '@angular/core';
import {Register} from '../register';

@Component({
  selector: 'app-info-card',
  templateUrl: './info-card.component.html',
  styleUrls: ['./info-card.component.css']
})
@Register()
export class InfoCardComponent implements OnInit {
  public called = "not called!";
  constructor() { }

  ngOnInit() {
    this.called = "called!";
  }
}

In the BoardComponent I add appRender directive to the element I want to display the components inside

The appRender handles the process of generating the components. I render the components based on JSON data.

  private components = [
  {
    "id": 0,
    "position": 0,
    "cells": [
      {
        "id": "info",
        "title": "Info panel",
        "component": "InfoCardComponent",
        "childs": []
      }
    ]
  }
];
  private componentsRef = {};

  constructor(
    private appRef: ApplicationRef,
    private injector: Injector,
    private componentFactory: ComponentFactoryResolver,
    private renderer: Renderer2,
    private el: ElementRef) {
  }

  ngOnInit() {
      this.el.nativeElement.innerHTML = '';
      const {rows} = this.render();
      for (const row of rows) {
        this.renderer.appendChild(this.el.nativeElement, row);
      }
  }

  render() {
    const grid = {rows: []};
    for (const c of this.components) {
      const cells = c.cells;
      if (cells.length > 0) {
        const row = this.renderer.createElement('div');
        for (const {component} of cells) {
          const cell = this.renderer.createElement('div');
          const card = this.renderer.createElement('div');
          this.renderer.appendChild(card, this.createAngularComponent(component).elem);
          this.renderer.appendChild(cell, card);
          this.renderer.appendChild(row, cell);
        }
        grid.rows.push(row);
      }
    }
    return grid;
  }
  private createAngularComponent(parentComponent) {
    const componentElem = this.componentFactory.resolveComponentFactory(getTypeFor(parentComponent)).create(this.injector);
    this.componentsRef[parentComponent] = componentElem;
    this.appRef.attachView(componentElem.hostView);
    return {
      elem: (componentElem.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement,
    };
  }

So my question is:

Is this an expected behavior or I forgot something?

I would be glad for any help.

etarhan
  • 4,138
  • 2
  • 15
  • 29
joka00
  • 2,357
  • 1
  • 10
  • 27

1 Answers1

2

You have an LifeCycleHook problem. You can't access ElementRef's and TemplateRef's in ngOnInit(), because the template is not yet rendered. You should use ngAfterViewInit() LifeCycleHook. Move your logic from ngOnInit() to ngAfterViewInit().

What to do:

Change ngOnInit() to ngAfterViewInit():

ngAfterViewInit() {
      setTimeout(() => {
        this.el.nativeElement.innerHTML = '';
        const {rows} = this.render();
        for (const row of rows) {
          this.renderer.appendChild(this.el.nativeElement, row);
        }
      }, 0)
  }

setTimeout() is for fixing ExpressionHasBeenChanged-Error.

Don't forget to implement AfterViewInit and to import that from @angular/core

Gregor Doroschenko
  • 11,488
  • 5
  • 25
  • 37
  • In my project I am calling it inside of `AfterViewChecked` why is the timeout also necessary there? – joka00 Apr 12 '19 at 23:53
  • 1
    @JosefKatič It is an ChangeDatection problem. In this article you will find more explanation and infos about that error and fix possibilities: https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4 – Gregor Doroschenko Apr 12 '19 at 23:57
  • For me the timeout will prevent the dynamic component from rendering. – Arwed Mett Mar 17 '22 at 14:02