'Angular/Leaflet/OpenStreetMap - clicking on pin for pop-up throws error about an already defined service variable, doesn't display binded pop-up

The error appears in the browser, when I click on any pin. No pop-up is displayed, nothing happens, just the error in the console.

enter image description here

Below you will find the component that displays the map and the pins and it should display the pop-up. The service is injected in the constructor.

The same service variable is successfully used when populating the map with pins.

import { Component, OnInit } from '@angular/core';
import * as L from 'leaflet';
import { PowerPlantService } from 'src/app/services/power-plant.service';
import { PowerPlant } from 'src/app/models/power-plant.model';
import { Observable } from 'rxjs';

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

    //coordinates for Brasov
  private latitude: number = 45.6427;
  private longitude: number = 25.5887;
  
  marker!: CustomPinMarker ;
  
  private map!: L.Map;
  private centroid: L.LatLngExpression = [this.latitude, this.longitude];
  powerPlantList!: PowerPlant[];

  ngOnInit(): void {
    this.initMap();
  }
  
  constructor(private powerPlantService: PowerPlantService) {

  }

  private initMap(): void {
    this.map = L.map('map', {
      center: this.centroid,
      zoom: 2.8
    });

    const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 
    {
      minZoom: 2.8,
      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    });

    tiles.addTo(this.map);
  
     /*
    the get request made through the HttpClient call will generate an Observable type to which we have to subscribe and unsubscribe at the end.
    */

    this.powerPlantService.getAll().subscribe(powerPlantArrayFromGet => {
      console.log(powerPlantArrayFromGet);
      this.powerPlantList = powerPlantArrayFromGet;
      this.powerPlantList.forEach( (element) => {
          if (element.published) {
            this.marker = new CustomPinMarker([element.latitude!, element.longitude!], element.gppd_idnr).on('click', this.onPinClick).addTo(this.map);
                       
          }          
      });
    })
  }

  onPinClick() : void{
       console.log("In pinclick method");
       let observableVar! : Observable<PowerPlant>;
       let powerPlant! : PowerPlant;
       observableVar = this.powerPlantService.findByGPPD_INDR(this.marker.getId());

       observableVar.subscribe(data => {powerPlant = data});
       this.marker.bindPopup(""+ powerPlant.powerPlant_name + '<br\>' + powerPlant.country_name + '<br\>' + powerPlant.est_generation_gwh_2017
      + '<br\>' + powerPlant.primaryFuel ).openPopup();
  }
}

export class CustomPinMarker extends L.Marker {
   gppd_idnr: String | undefined;

  constructor(latLng: L.LatLngExpression, gppd_idnr: string | String | undefined, options?: L.MarkerOptions) {
    super(latLng, options);
    this.gppd_idnr = gppd_idnr;
  }

  getId() : any{
    return this.gppd_idnr;
  }
}


Solution 1:[1]

This is the correct answer after some modifications:

import { Component, OnInit } from '@angular/core';
import * as L from 'leaflet';
import { PowerPlantService } from 'src/app/services/power-plant.service';
import { PowerPlant } from 'src/app/models/power-plant.model';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
 //coordinates for Brasov
  private latitude: number = 45.6427;
  private longitude: number = 25.5887;
   
  private map!: L.Map;
  private centroid: L.LatLngExpression = [this.latitude, this.longitude];
  powerPlantList!: PowerPlant[];

  ngOnInit(): void {
    this.initMap();
  }
  
  constructor(private powerPlantService: PowerPlantService) {  }

  private initMap(): void {
    this.map = L.map('map', {
      center: this.centroid,
      zoom: 2.8
    });

    const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 
    {
      minZoom: 2.8,
      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    });

    tiles.addTo(this.map);
  
    // this queries the database and retrieves a list of all the elements
    this.powerPlantService.getAll().subscribe(powerPlantArrayFromGet => {
      console.log(powerPlantArrayFromGet);
      this.powerPlantList = powerPlantArrayFromGet;
   // this loops through the list and creates and places a pin on the map at the location of each element
      this.powerPlantList.forEach( (element) => {
          if (element.published) {
            const that = this;
            //console.log(element.gppd_idnr + " 1")  ;  
            // this create the pin to be placed on the map and attaches to it a pop-up that, when clicked, will retrieve from the database information about that item and display it on the pop-up      
            let marker = new L.Marker([element.latitude!, element.longitude!])
              .on('click', function() {
                let observableVar! : Observable<PowerPlant>;
                let powerPlant! : PowerPlant;
                //console.log(element.gppd_idnr + " 2");
                observableVar = that.powerPlantService.findByGPPD_INDR(element.gppd_idnr + "");
            // the is where the interogation is done (above) and the pop-up is populated with the relevant information
                observableVar.subscribe(
                  data => 
                  {
                    powerPlant = data
            
                    var latlong : L.LatLng = new L.LatLng(<number> powerPlant.latitude, <number> powerPlant.longitude);

                    marker.setLatLng(latlong).bindPopup( 
                      powerPlant.powerPlant_name + 
                      '<br\>' + 
                      powerPlant.country_name + 
                      '<br\>' + 
                      powerPlant.est_generation_gwh_2017 + 
                      '<br\>' + 
                      powerPlant.primaryFuel )
                      .openPopup();
                  })
              })
              .addTo(that.map);                       
          }          
      });
    })
  }
}

Solution 2:[2]

As mentioned in my comment, the problem lies with misunderstanding what this means within your click handler. When using

this.marker = new CustomPinMarker([element.latitude!, element.longitude!], element.gppd_idnr)
  .on('click', this.onPinClick)
  .addTo(this.map);

You are passing the content of your function onPinClick to your event handler. Within your event handler this is the element that triggered the event. So it is the CustomPinMarker DOM object.

You can however circumvent this by wrapping your function in an arrow function to pass it to your event handler.

this.marker = new CustomPinMarker([element.latitude!, element.longitude!], element.gppd_idnr)
  .on('click', () => {
      this.onPinClick();
  })
  .addTo(this.map);

That works because arrow functions do not have their own this binding. These functions are resolved in the scope where they are declared in. In your case the HomeComponent.

Keep in mind, that by doing so you obviously lose the ability to access the triggering element. In case you need the element you could alias your HomeComponent and use that within an anonymous function in your event handler:

// that = HomeComponent, this = triggering DOM element
const that = this;
this.marker = new CustomPinMarker([element.latitude!, element.longitude!], element.gppd_idnr)
  .on('click', function() {
     let observableVar! : Observable<PowerPlant>;
     let powerPlant! : PowerPlant;
     observableVar = that.powerPlantService.findByGPPD_INDR(that.marker.getId());

   observableVar.subscribe(data => {powerPlant = data});
   that.marker.bindPopup(""+ powerPlant.powerPlant_name + '<br\>' + powerPlant.country_name + '<br\>' + powerPlant.est_generation_gwh_2017
  + '<br\>' + powerPlant.primaryFuel ).openPopup();
  })
  .addTo(that.map);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Art
Solution 2