3 min read

Rails 7, Leaflet and Import Maps with ESM and Stimulus

How to add Leaflet to Rails 7 with import maps
Rails 7, Leaflet and Import Maps with ESM and Stimulus
Photo by T.H. Chia / Unsplash

How to add Leaflet to Rails 7 with import maps.

I reach for Rails whenever I have a side project that I want to spin up quickly because it's just so magically productive. Hotwire is simply amazing – I ❤️ Turbo Frames.

I wanted to add some beautiful Leaflet maps to my Rails app using import maps rather than wrestling with the old asset pipeline. Here are the steps I took.

Pinning the libraries

Firstly, the Leaflet libraries need to be pinned and importmap can work out the links to a CDN.

bin/importmap pin leaflet
bin/importmap pin leaflet-css

This adds these two lines to the importmap.rb file (your version may vary, naturally):

# config/importmap.rb
# ...

pin "leaflet", to: "https://ga.jspm.io/npm:leaflet@1.9.4/dist/leaflet-src.js"
pin "leaflet-css", to: "https://ga.jspm.io/npm:leaflet-css@0.1.0/dist/leaflet.css.min.js"

Create a Stimulus controller

I'm going to wire up Leaflet using another part of Hotwire, Stimulus, because it makes sprinkling in JavaScript so much less gross. I'm using a Rails generator to scaffold a stimulus controller called map.

rails generate stimulus map

Now I'm going to edit the controller so that it creates a Leaflet map on the connect lifecycle callback. This is fired whenever the stimulus controller is connected to the DOM.

// app/javascript/controllers/map_controller.js

import { Controller } from "@hotwired/stimulus"
import "leaflet"
import "leaflet-css"

// Connects to data-controller="map"
export default class extends Controller {
  connect(){
    var map = L.map('map').setView([54, -4], 6);

    var tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);
  }

  disconnect(){
    this.map.remove()
  }
}

This imports both leaflet and leaflet-css modules, creates a map roughly centred on the UK and adds Open Street Map tiles (with attribution). The L.map('map') will place the map in a div with the id of map

Update the view

Now I need a place in the HTML to attach the map to. I use Haml as my templating engine.

#map.leaflet-container{ style: 'min-height: 400px' }
  %div{ data: { controller: 'map' } } 

This creates a div with the id of map which matches the id the JS is going to look for. It's styled with leaflet-container class. The div has an explicit minimum height, without which the div will be zero height.

An inner div wires up the stimulus controller.

That's it. A lovely map, a JavaScript library and not a bit of the asset pipeline in sight and all with delicious separation of concerns.

...and a bit further

I actually want to add a bit of GeoJSON to draw a region on the map. This is how I tackled it.

My Rails controller has an action which renders the GeoJSON as a JSON object.

# app/controllers/places_controller.rb

class PlacesController < ApplicationController
  def geometry
    render json: geo_str(params[:id]) # some magic to get the bounds
  end
end

and I have an entry in routes.rb which gives me the geometry_place_path helper.

I'm going to pass the URL for this into the stimulus controller and use JavaScript to fetch the contents. For this, I'll use a stimulus value attribute:

#map.leaflet-container{ style: 'min-height: 400px' }
  %div{ data: { controller: 'map', 'map-geourl-value': geometry_plave_path(@place_id) } } 

Finally, change the stimulus controller to use the value and fetch the data:

import { Controller } from "@hotwired/stimulus"
import "leaflet"
import "leaflet-css"

// Connects to data-controller="map"
export default class extends Controller {

  static values = { geourl: String }

  connect(){
    var map = L.map('map').setView([54, -4], 6);

    var tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);

    var geo = fetch(this.geourlValue) 
      .then( function (response) {
        return response.json();
      })
      .then( function (json) {
        var place = L.geoJson(json).addTo(map);
        map.fitBounds(place.getBounds());
      })
    
  }

  disconnect(){
    this.map.remove()
  }
}