How to bind Google Maps Javascript API library?

bsb
interop

#1

Hello, everyone.

I’ve found a nice blog post about how to make bindings from a JS library to Reason. However, this post assumes that this library is installed via NPM and is, thus, importable at compile time.

However, Google Maps Javascript API don’t work this way. Instead, it works by importing the api script through <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"></script> in the bottom of <body>, passing a function name to the import url (initMap, in the example) which will be called when the script is imported, and the API functions will only be accessible inside this callback.

I have no idea how I can do this with BSB/Reason. Is there a way?

Thanks, in advance.


#2

Hey @mateusfccp! I haven’t worked with Google Maps API and Reason. It would help to know more about your project. Is this a ReasonReact project?

Writing bindings for the Google Maps API may look something like this:

[@bs.deriving abstract]
type x = {
  lat: float,
  lng: float
};

[@bs.deriving abstract]
type y = {
  center: x,
  zoom: int
};

let coord = x(~lat=-34.397, ~lng=150.644);
let rule = y(~center=coord, ~zoom=8);

[@bs.val][@bs.scope ("document")] external getElementById : string => unit = "";

[@bs.new] [@bs.scope ("google", "maps")] external map : (unit , y) => unit = "Map";
let initMap = () => {
  let map = map(getElementById("map"), rule
  );
};

Depending on your project implementation things may look different. You can see how it compiles to js here.

I wonder though if you can just add the api sourced script tag to your and also add your compiled js sourced script tag to your html file.

Some doc references that may help:


#3

Hello, @ben! I thank you for your answer!

I managed to make it kind of work with this, before you answered:

type map;
[@bs.new] [@bs.scope ("google", "maps")] external make : (Dom.element, Js.t({..})) => map = "Map";

let initMap = () => {
  let map = make(mapElement, {
"center": { "lat": -34.397, "lng": 150.644 },
"zoom": 0
  });
}; 

[%bs.raw {| window.initMap = initMap |}];

However, your answer is more complete, so I’m going to use it.

I’m indeed using ReasonReact, and the next step is to integrate it on a component, but I think I have an idea about how to do this.


#4

Glad I could help! :+1:


#5

Well, It did work, but now I have to access a function from the object that is returned.

For example, in JS:

var infowindow = new google.maps.InfoWindow({
    content: contentString
});

infowindow.open(map, marker);

In Reason:

type infoWindow;
type infoWindowOptions = {.
    "content": string,
};

[@bs.new] [@bs.scope ("google", "maps")] external makeInfoWindow : infoWindowOptions => infoWindow = "infoWindow";

let iw = imakeInfoWindow({
     "content": "Lorem Ipsum",
});

iw.open(m, mm);

The last line, executing the open command, don’t work.


#6

This is probably not the best way, but it should work:


[@bs.deriving abstract]
type cntnt = {
  content: string
};
type marker; 
type map;

let detail = cntnt(~content="Loem Ipsum");

[@bs.new] [@bs.scope ("google", "maps")] external infoWindow : cntnt => unit = "InfoWindow";
[@bs.val] [@bs.scope ("infoWindow")] external open_ : (map, marker) => unit = "open";
let iw = infoWindow(detail);

open_(map, marker);

You just need to have the actual map and marker types defined and have the values to pass into infoWindow.open


#7

It won’t work… If I call iw.open_ the compiler will throw:

The record field open_ can't be found

If I try to use open_ alone, the program compiles, but I get a runtime error:

TypeError: google.maps.infoWindow is not a constructor

#8

hmmm. did you use bs.new attribute for infoWindow? Can you show me what your bindings look like? Also you don’t need to use dot notation for open_ on reason code. If you are using bs.scope attribute for open_ correctly it should compile to infoWorld.open in js.


#9

The code is split in two files. The first one, where I bind the functions:

[...]

[@bs.new] [@bs.scope ("google", "maps")] external makeMap : Dom.element => mapOptions => map = "Map";
[@bs.new] [@bs.scope ("google", "maps")] external makeMarker : markerOptions => marker = "Marker";
[@bs.new] [@bs.scope ("google", "maps")] external makeInfoWindow : infoWindowOptions => infoWindow = "infoWindow";
[@bs.val] [@bs.scope ("infoWindow")] external openInfoWindow : map => marker => unit = "open";

let initMap = () => {
    APISpots.renderByQuery(".una-reason__spots", ~context={
        map: makeMap,
        marker: makeMarker,
        infoWindow: makeInfoWindow,
        openInfoWindow,
    });
};

[%bs.raw {| window.initMap = initMap |}];

The functions are then passed to a ReasonReact component, that use them as follows:

[...]

module Models {
    type coordinates = {. "lat": float, "lng": float};
    
    type map;
    type mapOptions = {.
        "center": coordinates,
        "zoom": int
    };

    type marker;
    type markerOptions = {.
        "title": string,
        "position": coordinates,
        "map": map,
    };

    type infoWindow;
    type infoWindowOptions = {.
        "content": string,
    };

    type context = {
        map: Dom.element => mapOptions => map,
        marker: markerOptions => marker,
        infoWindow: infoWindowOptions => infoWindow,
        openInfoWindow:  map => marker => unit,
    };
};

[...]

didUpdate: _ => {

        let {Models.map, Models.marker, Models.infoWindow, Models.openInfoWindow} = switch(context) {
        | Some(fn) => fn
        | None => raise(NoFunction("You may pass Google Maps Javascript API context through `context` prop!"))
        };

        let el = Utils.DOM.getElementById("una-the-map");

        let m = map(el, {
            "center": {
                "lat": float_of_string(latitude),
                "lng": float_of_string(longitude),
            },
            "zoom": zoom,
        });

        let spots = switch (spots) {
        | Some(spots) => spots
        | None => []
        };

        let markers = spots
            |> List.filter(spot => spot.latitude !== None && spot.longitude !== None)
            |> List.map(spot => marker({
                "position": {
                    "lat": float_of_string(Js.Option.getExn(spot.latitude)),
                    "lng": float_of_string(Js.Option.getExn(spot.longitude)),
                },
                "map": m,
                "title": spot.display_name,
            })
        );

        let iw = infoWindow({
            "content": "Lorem Ipsum",
        });

        openInfoWindow(m, markers |> List.hd);

        ();
    },

[...]

I think it’s enough for you to have an idea, as the rest of the code doesn’t use the binded functions.

The map and marker functions worked with no problem, also infoWindow raised no error until I bound infoWindow.close the way you suggested.


#10

I would actually define map and marker types.


#11

I don’t think it will actually solve my problem. Anyway, Google documentation don’t provide the full specification of the types.


#12

I don’t think you need the Google Maps API to tell you the full specs. Just define the types to how you want it compiled in js.

I wonder if this will help. You can see how it compiles. For sake of simplicity I have defined map and marker types as string. I am not getting any TypeError.


#13

I got it to work by using:

[@bs.send] external openInfoWindow : infoWindow => map => marker => unit = "open";

Then, I can call openInfoWindow (infoWindow, map, marker) or infoWindow |> openInfoWindow(map, marker).

Thanks!