简体   繁体   中英

How to use BingMaps with Aurelia

I am using aurelia skeleton typescript webpack template for my app. I want to add BingMaps to a page and be able to add pushpins etc. So far I've done this:

index.html - I added a script tag that loads the map from CDN

<head>
...
   <script type='text/javascript' src='http://www.bing.com/api/maps/mapcontrol'></script>
</head>

then I added a template like this:

map.html

<template>
   <div id='mainMap' style='width: 100vw; height: 100vh;'></div>
</template>

then I have the map controller:

map.ts

export class Map {

    private map: Microsoft.Map.Map;

    attached() {
        this.map = new Microsoft.Maps.Map('mainMap', { credentials:'mycredentials - omitted'});
        ...
    }
}

Now, if I start the application the welcome screent (from the skeleton) is displayed. Then I click on the 'Map' menu link and the map page is show with the map fully loaded. But if I click refresh in browser (F5 or Ctrl+F5) the map is not show anymore and an error is displayed in the console:

bluebird.js:1546 Unhandled rejection TypeError: Cannot read property 'prototype' of null at k ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:7096 ) at h ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:6285 ) at e ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:1106 ) at tl [as instance] ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:161 ) at h ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:6042 ) at e ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:1106 ) at tl [as instance] ( https://www.bing.com/mapspreview/sdk/mapcontrol:11:161 ) at new Microsoft.Maps.Map ( https://www.bing.com/mapspreview/sdk/mapcontrol:13:4304 ) at Map.attached ( http://localhost:9000/app.bundle.js:31267:20 ) at Controller.attached ( http://localhost:9000/aurelia.bundle.js:6438:22 ) at View.attached ( http://localhost:9000/aurelia.bundle.js:4524:23 ) at ViewSlot.attached ( http://localhost:9000/aurelia.bundle.js:4883:13 ) at View.attached ( http://localhost:9000/aurelia.bundle.js:4 534:19 ) at ViewSlot.attached ( http://localhost:9000/aurelia.bundle.js:4883:13 ) at http://localhost:9000/aurelia.bundle.js:14717:28

This error is thrown when it is trying to instantiate the Map object in the Attached event of the map controller.

Why is this happening and how can I solve this? Please help

Thanks

The key to solving this issue is to use the callback parameter that the API allows us to specify in the script url. I've created a custom element to do this. The script is loaded when the module is initially loaded. Any instances of the custom element will wait for the callback function to be called. I originally was using a convoluted setup with a MutationObserver and data attributes, but after to talking to Jeremy Danyow, he pointed out that "Promisifying" the callback will solve the solution much more simply. This simpler solution is shown below.

The custom element currently doesn't offer any API for interacting with the map, other than getting the current center point of the map, but it's a good starting point to help you.

bing-map.ts

import { bindable, bindingMode, inlineView } from 'aurelia-framework';

const controlUrl = '//www.bing.com/api/maps/mapcontrol?callback=bingMapsLoaded';
const ready = new Promise(resolve => window['bingMapsLoaded'] = resolve);

let scriptTag: HTMLScriptElement = document.createElement('script');

scriptTag.async = true;
scriptTag.defer = true;
scriptTag.src = controlUrl;

document.head.appendChild(scriptTag);

@inlineView('<template><div ref="container" css="width: ${width}; height: ${height};"></div></template>')
export class BingMapCustomElement {
  private container: HTMLElement;
  private map: Microsoft.Maps.Map;
  private viewChangeHandler: Microsoft.Maps.IHandlerId;

  @bindable apiKey = '';
  @bindable height = '600px';
  @bindable width = '400px';

  @bindable({ defaultBindingMode: bindingMode.twoWay }) location: Microsoft.Maps.Location | string;

  attached() {
    return ready.then(() => {
      this.map = new Microsoft.Maps.Map(this.container as HTMLElement, {
        credentials: this.apiKey
      });

      this.location = this.map.getCenter();

      this.viewChangeHandler = Microsoft.Maps.Events.addHandler(this.map, 'viewchange', e => {
        this.location = this.map.getCenter();
      });
    });
  }

  detached() {
    if (this.viewChangeHandler) {
      Microsoft.Maps.Events.removeHandler(this.viewChangeHandler);
    }

    if (this.map) {
      this.map.dispose();
      this.map = null;
    }
  }
}

Usage

<bing-map api-key.bind="mapsApiKey" width="100px" height="100px"></bing-map>

This is because you're trying to instantiate the map before the external script has fully loaded. It works in your first scenario but doesn't work when you do a direct refresh.

There's a great solution posted here:

How to wait BingMaps to be loaded in Aurelia

The problem with the callback solution is that you're loading the external script in index.html, which is blind to whether or not you're currently wanting to display a map. So the second solution is more appropriate. However, I think the solution was written for ESNext code and you're using TypeScript, which means that the typeof properties are never going to be undefined. The following code for map.ts would work better in your context (you might have to debug it a little since I wasn't able to test it):

export class Map
{
  map:Microsoft.Maps.Map;

  attached() {
    this.loadMap();
  }

  loadMap() {
    if ((Microsoft == null) || (Microsoft.Maps == null)) {
      // not yet available, keep trying (dirty checking)
      setTimeout(this.loadMap, 100);
    } else {
      // Map API available; proceed to render the map
      this.map = new Microsoft.Maps.Map('#mainMap', {credentials: myKey});
      this.map.setView({center: new Microsoft.Maps.Location(45.093,14.114), zoom:15});
    }
  }
}

Note the if ((Microsoft == null) || (Microsoft.Maps == null)) test instead of the undefined test.

Another great solution (by Jason Sobell)

http://www.sobell.net/calling-external-javascript-from-within-aurelia-templates/

A third great solution (after some callback research)

You can implement this without the external script link in index.html , since this should provide the loading mechanism dynamically.

export class Map
{
  private map: any;

  attached() {
    this.loadScript("//www.bing.com/api/maps/mapcontrol?onload=callback");
  }

  loadScript(sScriptSrc) {
    var oHead = document.getElementsByTagName("head")[0];
    var oScript = document.createElement('script');
    oScript.type = 'text/javascript';
    oScript.src = sScriptSrc;
    oHead.appendChild(oScript);
    oScript.onload = this.loadMap();
  }

  loadMap() {
    // Map API available; proceed to render the map
    this.map = new Microsoft.Maps.Map('#mainMap', {credentials: myKey});
    this.map.setView({center: new Microsoft.Maps.Location(45.093,14.114), zoom:15});
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM