Google Maps API with StimulusJS

Episode #236 by David Kimura

Summary

Add unobtrusive maps to your application using StimulusJS to tie in Google Maps Javascript API and Places API.
api javascript rails stimulusjs 17:14

Resources

Summary

# Terminal
rails g scaffold places name latitude:decimal longitude:decimal
rails webpacker:install:stimulus

# layouts/application.html.erb
<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?key=#{ENV['MAPS_API_KEY']}&libraries=places&callback=dispatchMapsEvent",
                            async: true,
                            defer: true,
                            "data-turbolinks-eval": false %>

# packs/application.js
window.dispatchMapsEvent = function (...args) {
  const event = document.createEvent("Events")
  event.initEvent("google-maps-callback", true, true)
  event.args = args
  window.dispatchEvent(event)
}

# javascript/controllers/maps_controller.js
import { Controller} from "stimulus"

export default class extends Controller {
  static targets = ["field", "map", "latitude", "longitude"]

  connect() {
    if (typeof (google) != "undefined"){
      this.initializeMap()
    }
  }

  initializeMap() {
    this.map()
    this.marker()
    this.autocomplete()
    console.log('init')
  }

  map() {
    if(this._map == undefined) {
      this._map = new google.maps.Map(this.mapTarget, {
        center: new google.maps.LatLng(
          this.latitudeTarget.value,
          this.longitudeTarget.value
        ),
        zoom: 17
      })
    }
    return this._map
  }

  marker() {
    if (this._marker == undefined) {
      this._marker = new google.maps.Marker({
        map: this.map(),
        anchorPoint: new google.maps.Point(0,0)
      })
      let mapLocation = {
        lat: parseFloat(this.latitudeTarget.value),
        lng: parseFloat(this.longitudeTarget.value)
      }
      this._marker.setPosition(mapLocation)
      this._marker.setVisible(true)
    }
    return this._marker
  }

  autocomplete() {
    if (this._autocomplete == undefined) {
      this._autocomplete = new google.maps.places.Autocomplete(this.fieldTarget)
      this._autocomplete.bindTo('bounds', this.map())
      this._autocomplete.setFields(['address_components', 'geometry', 'icon', 'name'])
      this._autocomplete.addListener('place_changed', this.locationChanged.bind(this))
    }
    return this._autocomplete
  }

  locationChanged() {
    let place = this.autocomplete().getPlace()

    if (!place.geometry) {
      // User entered the name of a Place that was not suggested and
      // pressed the Enter key, or the Place Details request failed.
      window.alert("No details available for input: '" + place.name + "'");
      return;
    }

    this.map().fitBounds(place.geometry.viewport)
    this.map().setCenter(place.geometry.location)
    this.marker().setPosition(place.geometry.location)
    this.marker().setVisible(true)

    this.latitudeTarget.value = place.geometry.location.lat()
    this.longitudeTarget.value = place.geometry.location.lng()
  }

  preventSubmit(e) {
    if (e.key == "Enter") { e.preventDefault() }
  }
}

# views/places/_form.html.erb
<%= form_with model: place, local: true, data: {
                                            controller: :maps,
                                            action: "[email protected]>maps#initializeMap"
                                          } do |form| %>
  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name, class: 'form-control' %>
  </div>

  <div class="field">
    <%= form.label :search %>
    <%= form.search_field :search, name: nil, class: 'form-control', data: { target: "maps.field", action: "keydown->maps#preventSubmit" } %>
  </div>

  <div class="field">
    <%= form.label :latitude %>
    <%= form.text_field :latitude, class: 'form-control', data: { target: "maps.latitude" } %>
  </div>

  <div class="field">
    <%= form.label :longitude %>
    <%= form.text_field :longitude, class: 'form-control', data: { target: "maps.longitude" } %>
  </div>

  <%= content_tag :div, nil, data: { target: "maps.map" }, class: 'map' %>

  <div class="actions">
    <%= form.submit class: 'btn btn-primary' %>
  </div>
<% end %>

# stylesheets/places.scss
.map {
  width: 100%;
  min-height: 400px;
}

# views/places/show.html.erb
<%= image_tag "https://maps.googleapis.com/maps/api/staticmap?zoom=15&size=400x300&center=#{@place.latitude},#{@place.longitude}&key=#{ENV['MAPS_API_KEY']}", alt: "Map" %>