javascriptangularscopeleafletclosures

@Output in child component is undefined when trying to emit Angular v17


I have a list component displaying objects with accordion elements. On expanding the accordion body, I use a (click)="addToArray(id)" method and have <edit-component *ngIf="idArray.includes(id)"></edit-component> in the accordion body.

The reason is to load the edit-component after expansion of the accordion, because it includes a leaflet map that didn't display properly when the accordion was collapsed initially.

Now, the edit-component includes some inputs and methods for handling and validating those, as well as a map-input-component. It's using Leaflet, and I had to put the .map-container in the edit-component template because it couldn't find the DOM element in it's own component..

I want this map component to emit where the user is clicking on the map, and have gotten as far as reading the leaflet event and wanting to emit it with an @Output to the edit-component parent.

However, when I try to access the this.mapClickEvent it's undefined.

I suspect this might be because the component is loaded on runtime?

Is my only option to include the map directly in the edit-component instead?

edit.component.ts

@Component({
  selector: "edit-component",
  templateUrl: "./edit.component.html"
})
export class EditComponent {
  @Input() myObject!: myObjectInterface;

  // Methods for other inputs

  onMapClick(event: {lat: number, lng: number}){
    console.log(event)
  }
}

edit.component.html

<div class="row">
  <!-- Other cols with inputs -->
  <div class="col">
    <div class="map-container" [attr.id]="'map-input-' + myObject.id"></div>
    <map-input-component
      [object]="myObject"
      (mapClickEvent)="onMapClick($event)"
    ></map-input-component>
  </div>
</div>

map-input.component.ts

@Component({
  selector: "map-input-component",
  template: "",
  standalone: true
})
export class MapInputComponent {
  @Input() object!: myObjectInterface;
  @Output() mapClickEvent = new EventEmitter<{lat: number, lng: number}>();

  // Leaflet map setup with L.on("click", this.onMapClick)

  onMapClick(e: LeafletMouseEvent) {
    // A console.log(e) logs the wanted data
    // A console.log(this.mapClickEvent) logs undefined
    this.mapClickEvent.emit({lat: e.latlng.lat, lng: e.latlng.lng});
  }
}

Error

ERROR TypeError: Cannot read properties of undefined (reading 'emit')
    at NewClass.onMapClick (map-input.component.ts:132:24)
    at NewClass.fire (leaflet-src.js:606:11)
    at NewClass._fireDOMEvent (leaflet-src.js:4565:17)
    at NewClass._handleDOMEvent (leaflet-src.js:4514:10)
    at HTMLDivElement.handler (leaflet-src.js:2799:15)
    at _ZoneDelegate.invokeTask (zone.js:398:33)
    at core.mjs:14556:55
    at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:14556:36)
    at _ZoneDelegate.invokeTask (zone.js:397:38)
    at Object.onInvokeTask (core.mjs:14869:33)

Solution

  • When you set the mapClickEvent to Leafset, make sure to bind it to the scope of the component, you can do this by using .bind(this).

    Wherever the function executes it will always have the scope of the component.

    L.on("click", this.onMapClick.bind(this))
    

    As per code shared, it should be:

    const leafletMap: Map = map(mapContainer).on("click", this.onMapClick.bind(this));