Skip to content

MapstedMapService — Angular injectable service

This recipe provides an injectable MapstedMapService that wraps the init / destroy lifecycle and makes the MapInstance available throughout your Angular application.

For the full tutorial covering Angular Universal SSR, @ViewChild, and NgModule vs standalone components, see Embed with Angular.

The service

typescript
// src/app/services/mapsted-map.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
import { init } from '@mapsted/maps-js-api';
import type { MapInstance } from '@mapsted/maps-js-api';

@Injectable({ providedIn: 'root' })
export class MapstedMapService {
  private mapInstance: MapInstance | null = null;

  /** Emits `true` once the map is fully loaded and ready for commands. */
  readonly ready$ = new BehaviorSubject<boolean>(false);

  constructor(@Inject(PLATFORM_ID) private platformId: object) {}

  /** Initialize the map inside the given container element. */
  async mount(element: HTMLElement, propertyId: number): Promise<void> {
    if (!isPlatformBrowser(this.platformId)) return;

    this.mapInstance = await init({
      element,
      propertyId,
      onload: () => this.ready$.next(true),
    });
  }

  /** Destroy the map and reset state. Call from `ngOnDestroy`. */
  async unmount(): Promise<void> {
    await this.mapInstance?.destroy();
    this.mapInstance = null;
    this.ready$.next(false);
  }

  /** Returns the raw MapInstance for direct API calls. */
  getInstance(): MapInstance | null {
    return this.mapInstance;
  }
}

Component using the service

Angular 17+ required

The @if control-flow syntax shown below requires Angular 17 or later. For Angular 14–16, replace @if (mapService.ready$ | async) { … } with <ng-container *ngIf="mapService.ready$ | async">…</ng-container> and add NgIf to the standalone component's imports: [AsyncPipe, NgIf].

typescript
// src/app/components/map-view/map-view.component.ts
import {
  Component,
  Input,
  ViewChild,
  ElementRef,
  AfterViewInit,
  OnDestroy,
} from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { MapstedMapService } from '../../services/mapsted-map.service';
import { navigateToFloorById } from '@mapsted/maps-js-api';

@Component({
  selector: 'app-map-view',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <div #mapContainer style="width: 100%; height: 100vh;"></div>
    @if (mapService.ready$ | async) {
      <button (click)="handleNavigate()">Go to floor</button>
    }
  `,
})
export class MapViewComponent implements AfterViewInit, OnDestroy {
  @Input() propertyId!: number;
  @ViewChild('mapContainer') mapContainer!: ElementRef<HTMLDivElement>;

  constructor(public mapService: MapstedMapService) {}

  async ngAfterViewInit(): Promise<void> {
    await this.mapService.mount(this.mapContainer.nativeElement, this.propertyId);
  }

  async ngOnDestroy(): Promise<void> {
    await this.mapService.unmount();
  }

  handleNavigate(): void {
    navigateToFloorById(42);
  }
}

Notes

  • providedIn: 'root': The service is a singleton. If you mount more than one map on the same page, create separate service instances using component-level providers: [MapstedMapService] instead of the root injector.
  • ready$ observable: BehaviorSubject<boolean> starts as false and emits true once onload fires. Bind it with the async pipe in templates to conditionally show controls.
  • SSR guard: The isPlatformBrowser guard in mount() ensures init() is only called in the browser, making the service safe for Angular Universal applications.
  • getInstance(): Returns the raw MapInstance so you can call any Maps JS API method directly. Import named functions from @mapsted/maps-js-api — they operate on the module-scoped singleton automatically.