1

I get an error on my Angular app kind of a stackOverflow. (see updates at the end)

Basically my component is a diagram that includes two components. The equipment component that includes the equipment information and the socket component, that includes the type of connections that each equipment has. The socket component basically displays a dropdown with the equipment available to connect. The "container" component basically creates a socket component with the type "start element" so we have a "fake" connection to select a start element. From then we select a start element and we can keep feeding the diagram. When I select the first "start element" (pic1) the corresponding equipment is loaded with its corresponding sockets (7 units). Then I select on one of the sockets one equipment (pic2) and the same, it is loaded with one unit, all good. Then I select one more equipment in the corresponding socket (pic3) and buum, the app crashes. Error on 3!

I get this error:

ERROR RangeError: Maximum call stack size exceeded
    at refreshView (core.js:9461)
    at refreshEmbeddedViews (core.js:10591)
    at refreshView (core.js:9490)
    at refreshComponent (core.js:10637)
    at refreshChildComponents (core.js:9263)
    at refreshView (core.js:9516)
    at refreshEmbeddedViews (core.js:10591)
    at refreshView (core.js:9490)
    at refreshComponent (core.js:10637)
    at refreshChildComponents (core.js:9263)

I made some console logs and at the click where the error happens, the first start element is rendered (ngOnInit), the second one as well and in the third one that I clicked, the Socket onInit and Equipment onInit are called with no end. So it seems that the equipment and the socket are calling themselves with no end.

I checked my app, and I call only two times the socket component. Once on the container component just to create the "start element" socket and then on the ngfor on EquipmentComponent. And regarding equipment, it is only called once in the whole app, once mySocket.element is selected and is not null, therefore the ngIf shows the component.

I only have one module app.module so here the same, there are no multiple calls to the components.

Here the socketcomponent:

import { Component, Input, OnInit } from '@angular/core';
import { Equipment } from 'src/app/model/Equipment';
import { Socket } from 'src/app/model/Socket';
import { EquipmentService } from 'src/app/services/equipment.service';

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

  @Input() mySocket: Socket;
  @Input() parentEquipment: Equipment;

  myElements: Equipment[];

  constructor( private _equipmentNewService:EquipmentService) { }

  ngOnInit(): void {
    console.log("Myparentequipment:", this.parentEquipment)
    this.myElements = this._equipmentNewService.getListForGivenInput(this.mySocket.type);
  }

  loadEquipment(eq:Equipment){
    this.mySocket.element = eq;
  }

  amIFirstConnection(){
    return !this.amIOnlyConnection() && this.mySocket === this.parentEquipment.eqType.connections[0];
  }

  amILastConnection(){
    return !this.amIOnlyConnection() && this.mySocket === this.parentEquipment.eqType.connections[this.parentEquipment.eqType.connections.length-1];
  }

  amIMiddleConnection(){
    return !this.amIOnlyConnection() && !this.amIFirstConnection() && !this.amILastConnection();
  }

  amIOnlyConnection(){
    return this.parentEquipment.eqType.connections.length===1
  }

}

Here the html file:

<!-- this is just to create the diagram lines, ignore it! -->
<div *ngIf="mySocket.type !== 'Start Element'">
    <div *ngIf="amIOnlyConnection()" class="d-flex someMarginTop">
        <div class="smallSpacer schema"></div>
        <div class="mainSpacer schema borderLeft"></div>
    </div>
    <div *ngIf="amIFirstConnection()" class="d-flex someMarginTop">
        <div class="smallSpacer schema"></div>
        <div class="mainSpacer schema borderLeft borderTop"></div>
    </div>
    <div *ngIf="amIMiddleConnection()" class="d-flex someMarginTop">
        <div class="smallSpacer schema borderTop"></div>
        <div class="mainSpacer schema borderTop borderLeft"></div>
    </div>
    <div *ngIf="amILastConnection()" class="d-flex someMarginTop">
        <div class="smallSpacer schema borderTop"></div>
        <div class="mainSpacer schema borderLeft"></div>
    </div>
</div>
<!-- here we display the info -->
<div class="dropdown someMargin">
    <button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
          {{mySocket.name}}...
        </button>
    <ul class="dropdown-menu listSocket" aria-labelledby="dropdownMenuButton1">
        <li *ngFor="let item of myElements">
            <div class="d-flex"><a class="dropdown-item" (click)="loadEquipment(item)"><span style="margin-right:5px">{{item.eqType.productNumber}}</span></a></div>
        </li>

    </ul>
    <!-- and here the call to equipment -->
    <div *ngIf="mySocket.element">
        <app-equipment [equipment]="mySocket.element" [parentSocket]="this.mySocket"></app-equipment>

    </div>
</div>

And now the equipment component:

import { Component, Input, OnInit } from '@angular/core';
import { Equipment } from 'src/app/model/Equipment';
import { Socket } from 'src/app/model/Socket';

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


  @Input() equipment: Equipment;
  @Input() parentSocket: Socket;

  constructor() {
   }

  ngOnInit(): void {
    console.log("Myparentsocket:", this.parentSocket)
    this.equipment.available = false;
  }

  removeMyselfFromSocket() {
    this.parentSocket.element = null;
  }

}

Here the html file:

<div class="someMargin somePadding  col-md-auto border rounded eqContainer animated fadeIn fast">
    <div style="float:right">
        <button type="button" class="btn btn-outline-danger px-3" style="padding: 0px 7px !important;" (click)="removeMyselfFromSocket()"><i class="fas fa-trash fa-xs"></i></button>
    </div>
    <div class="d-flex">
        <div>
            <!-- <img [src]="equipment.eqType.img" class="img-fluid imgTestModel" [alt]="equipment.eqType.productNumber"> -->
        </div>
        <div style="margin-left:5px" class="bg-light border rounded somePadding" style="margin-right:30px">
            <!-- <p class="title p-lessMargin text-primary">{{equipment.eqType.productNumber}}</p> -->
            <p class="ids p-lessMargin">ProductNO: <span class="ids-content">ABC8493RT</span> </p>
            <p class="ids p-lessMargin">SerialNO: <span class="ids-content">15698247896</span></p>
            <p class="ids p-lessMargin">SwRev: <span class="ids-content">2.2.145</span></p>
        </div>

    </div>
    <div class="d-flex">
        <app-socket *ngFor="let socket of equipment.eqType.connections" [mySocket]="socket" [parentEquipment]="equipment"></app-socket>
    </div>

</div>

I hope I attached enough information and I would be very happy to get some help!

Thanks in advance :)

Update 1: After changing on socket component this method (to avoid the change detector being called the whole time):

equipmentSelected:Equipment;
loadEquipment(eq:Equipment){
    this.equipmentSelected = eq;
  }

I can now add more childs to my tree. Now the question is, how can I do to keep the this.mySocket.element = eq? Otherwise I would lose my tree structure and would not be able to serialize my objects...

5
  • It's not that easy to recognize the issue. I see 2 main places which could be the reason: mutations and functions in the template are not that easy handled by the Angular change detector Commented Jun 24, 2021 at 8:56
  • it's not that easy and short topic. requires time to comprehend. this article would be of help: blog.angular-university.io/… Commented Jun 24, 2021 at 8:58
  • Thanks a lot for the link to the article! After reading it I made one change. In the socket component in the function loadEquipment(), instead of this.mySocket.element = eq; I created a new variable and did like this: this.equipmentSelected = eq; Now I need to figure out how to keep the value on this.mySocket.element, otherwise I would lose the tree structure. Do you have any ideas? Commented Jun 24, 2021 at 10:03
  • 1
    You have 2 components referencing each other. This is almost certainly the reason you have a stack overflow. I'd take a good look at how you're structuring your app and consider a restructure to avoid this circular reference. Commented Jun 24, 2021 at 10:42
  • So finally in loadEquipment() I just used a clone of the object like this: this.mySocket.element = JSON.parse(JSON.stringify(eq)); and now it works. I do not fully understand why it works this way, it would be nice to get a nice answer from one of you guys! :) Commented Jun 28, 2021 at 6:20

1 Answer 1

0

with the help of one good friend I found the solution. It was on the method loadEquipment() located on the socket component. As it changes the object mySocket which is created on this component, the component is rendered again and as it is an input from the parent socket, the parent socket equipment component is also rendered again creating a stack overflow as the components referencing each other are loading the whole time.

What I needed to to is change the method to this:

loadEquipment(eq:Equipment){
    // js workaround for cloning
    this.mySocket.element = JSON.parse(JSON.stringify(eq));
} 

Creating a clone of the object lets the change detector stay quiet and avoid re-rendering.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.