#Undefined when trying to access a class field, value shows up in the constructor

23 messages · Page 1 of 1 (latest)

naive iris
#

Hello everyone,

I've been working in Java with Spring/Spring-boot for many years, but I'm pretty new to Angular and I've been trying to explore the tech with a hobby project recently. I've succesfully developped a small website fetching data from my spring-boot backend and displaying them in a hierarchy.

The data contains a few different entities, say a System root for the hierarchy, then under the System, there are a few planets/stars entities. What I'd like to do is to display an info-block depending on the entity clicked on by the user. I've centralized this through an EventService which shows an Entity object (abstract class to the three I mentionned) which can be subscribed to by components. I read about Dynamic Components on the documentation here : https://angular.io/guide/dynamic-component-loader, which gave me enough to put up something that - should - work, and to narrow down the issue to something allowing me to come here for help.

To link Entities to their related EntityInfoComponent (i.e. System to SystemInfoComponent), I've added an abstract field to the Entity class of Type<EntityInfoComponent>, which is then set for every concrete Entity. In the constructor, whenever I call console.log(component), I can succesfully show the relevant component in the console. However, when I try to access it through the component hierarchy using a Directive to insert it via viewContainerRef.createComponent<EntityInfoComponent>, the component field is undefined. There is probably a lifecycle issue I'm not aware of, but I'm a bit lost about what to do to fix the issue.

Does anyone happen to have an idea about it ?

vague lodge
#

The code matters. Post the code.

naive iris
#

There you go :

Service fetching the data :

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Simulation } from '../entities/simulation';

const baseUrl = 'http://localhost:8080/simulation/';

const httpOptions = {
  headers: new HttpHeaders(
    {
      'Content-Type': 'application/json',
    }
  )
};

@Injectable({providedIn: 'root'})
export class SimulationService {

  constructor(private http: HttpClient) { }

  public getBaseSimulation(): Observable<Simulation> {
    return this.http.get<Simulation>(baseUrl);
  }

}

Entity class holding the data :

import { SystemInfoComponent } from '../components/infos/system-info/system-info.component';
import { EntityInfoComponent } from '../components/infos/entity-info/entity-info.component';
import { Type } from '@angular/core';
import { Entity } from './entity';
import { Star } from './star';
import { Planet } from './planet';

export class System extends Entity {

  component: Type<EntityInfoComponent> = SystemInfoComponent;

  name: string = '';
  type: string = '';

  periodeHabitable: string = '';
  distanceHabitable: string = '';

  stars: Star[] = [];
  planets: Planet[] = [];

  constructor() {
    super();
    console.log(this.component);
  }

}
#

Info-block component, responsible for injecting the right template depending on the entity currently selected :

import { Component, OnInit, ViewChild } from '@angular/core';
import { InfoBlockDirective } from '../../directives/info-block.directive';
import { EntityInfoComponent } from '../infos/entity-info/entity-info.component';
import { SystemInfoComponent } from '../infos/system-info/system-info.component';
import { EventService } from '../../services/event.service';
import { Entity } from '../../entities/entity';

@Component({
  selector: 'app-info-block',
  templateUrl: './info-block.component.html',
  styleUrls: ['./info-block.component.css']
})
export class InfoBlockComponent implements OnInit {

  @ViewChild(InfoBlockDirective, {static: true}) infoBlock!: InfoBlockDirective;
  selectedEntity!: Entity;

  constructor(private eventService: EventService) { }

  ngOnInit(): void {
    this.subscribeToSelection();
  }

  loadComponent(): void {
    const viewContainerRef = this.infoBlock.viewContainerRef;
    viewContainerRef.clear();
    console.log('cleared');
    if (this.selectedEntity != null) {
      const componentRef = viewContainerRef.createComponent<EntityInfoComponent>(this.selectedEntity.component);
      componentRef.instance.entity = this.selectedEntity;
      console.log('loaded');
    }
  }

  subscribeToSelection(): void {
    this.eventService.entitySelected.subscribe(entity => {
      console.log('updated');
      this.selectedEntity = entity;
      this.loadComponent();
    });
  }

}

EventService, emitting events when an entity has been selected when clicking on it :

import { Injectable, EventEmitter } from '@angular/core';
import { Observable } from 'rxjs';
import { Entity } from '../entities/entity';

@Injectable({providedIn: 'root'})
export class EventService {

  entitySelected = new EventEmitter<Entity>();

  public selectEntity(entity: Entity) {
    console.log('emitted');
    this.entitySelected.emit(entity);
  }

}
#

Result displayed in the console :

#

Undefined noted using a breakpoint :

vague lodge
#

What you posted isn't sufficient for me to understand where this selectedEntity come from. Post a complete minimal reproduction on Stackblitz.

naive iris
#

Ah, here is the node list component, clicking on a node triggers "selectNode" :

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { EventService } from '../../services/event.service';
import { System } from '../../entities/system';
import { Entity } from '../../entities/entity';

@Component({
  selector: 'app-node-list',
  templateUrl: './node-list.component.html',
  styleUrls: ['./node-list.component.css']
})
export class NodeListComponent implements OnInit {

  @Input() system: System;
  selectedEntity: Entity | undefined;

  constructor(private eventService: EventService) {
    this.system = new System();
  }

  ngOnInit(): void {
    this.subscribeToSelection();
  }

  selectNode(entity: Entity) {
    this.eventService.selectEntity(entity);
  }

  subscribeToSelection(): void {
    this.eventService.entitySelected.subscribe(entity => {
      this.selectedEntity = entity;
    });
  }

}

<div *ngIf="system">

  <div hover-class="hovered" [ngClass]="{'selected' : system.id === selectedEntity?.id}" class='system-node node' (click)="selectNode(system)">
    <app-system-node [system]="system"></app-system-node>
  </div>

  <div class='split'></div>
     
  <div hover-class="hovered" [ngClass]="{'selected' : star.id === selectedEntity?.id}" class='star-node node' *ngFor="let star of system.stars" (click)="selectNode(star)">
    <app-star-node [star]="star"></app-star-node>
  </div>

  <div class='split'></div>

  <div hover-class="hovered" [ngClass]="{'selected' : planet.id === selectedEntity?.id}" class='planet-node node' *ngFor="let planet of system.planets" (click)="selectNode(planet)">
    <app-planet-node [planet]="planet"></app-planet-node>
  </div>

</div>
#

I'll work on the Stackblitz in the meantime

vague lodge
#

It seems the selected entity is part of the System returned by the HTTP get. The HttpClient will never create instances of your class. All it does is JSON.parse() to transform the body into an object. So wht you get is a plain old JavaScript object, which isn't an instance of any class.

naive iris
#

Oh, okay I see. Is there a way for these js objects to go through the normal lifecycle of an instance ?

#

Or if you understand the bigger picture about what I want to do, a way for me to correctly instantiate the right component depending on the entity I want to display ?

vague lodge
#

You'd need to map the JS object you get into an instance of your System class (and you would thus also need to map every of the planet POJOs into a Planet instance, etc., recursively)

naive iris
#

Alright, looks enough for me to look into it. Last question about the whole thing, do you have advices about a better way to achieving that dynamic component ?

vague lodge
#

I don't know how many different entities you have. My guess is two or three. I would just have a type property for each entity in the JSON, and use an ngIf or ngSwitch to display the appropriate component, instead of all this dynamic stuff. Something like:

interface System {
  entities: Array<Entity>;
}
interface Planet {
  type: 'planet';
  name: string;
  // ...
}
interface Star {
  type: 'star';
  // ...
}
type Entity = Planet | Star;

and in the HTML

<div *ngFor="let entity of system.entities">
  <planet *ngIf="entity.type === 'planet'" [planet]="entity"></planet>
  <star *ngIf="entity.type === 'star'" [star]="entity"></star>
</div>
naive iris
#

I do have a few entities for now, but I plan to have many more, however a simpler solution looks fine to me as it's a small hobby project

#

Also the type field is already set currently so not much more to do actually

#

This is the way I did the node list menu you can see here :

#

If I understood you correctly, on the "info" part, where I want only a single info block to be displayed, I'd use ng-if on the current selectedEntity and list all different entities with their related component for the info component to display at runtime when it's updated through selectedEntity event emit, correct ?

#

ng-if type == 'planet' => <app-planet-info-component>
ng-if type == 'system' => <app-system-info-component>
and so on

vague lodge
#

Yes.

naive iris
#

Alright, thank you very much for the insight, have a nice day 🙂

vague lodge
#

You too. Good luck.