DEV Community

Cover image for Implement Leaflet on rails with stimulus
Ankit Pariyar
Ankit Pariyar

Posted on

Implement Leaflet on rails with stimulus

Set up basic rails app

  • we will use database PostgreSQL, esbuild to bundle the project's JavaScript, and TailwindCSS for its CSS. We'll also skip installing a test framework by passing the -T flag.
rails new leaflet_map -T -d postgresql --css=tailwind --javascript=esbuild
cd leaflet_map
rails db:create
Enter fullscreen mode Exit fullscreen mode
  • Configure Tailwind: In application.tailwind.css replace the @tailwinddirectives with @import
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
Enter fullscreen mode Exit fullscreen mode

Create basic MVC

  • create Place model which will have longitude, latitude and name attributes
rails g model Place name:string longitude:float latitude:float
rails db:migrate
Enter fullscreen mode Exit fullscreen mode
  • add simple validation for name, longitude and latitude on Place model
class Place < ApplicationRecord
  validates :name, presence: true
  validates :latitude, numericality: { greater_than_or_equal_to:  -90, less_than_or_equal_to:  90 }
  validates :longitude, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }
end
Enter fullscreen mode Exit fullscreen mode
  • update seed.rb file to create some data for places
Place.create(name: 'Place-1', longitude: -70.06, latitude: 39.35)
Place.create(name: 'Place-2', longitude: -55.30, latitude: 35.20)
Place.create(name: 'Place-3', longitude: -80.20, latitude: 25.20)
Place.create(name: 'Place-4', longitude: -90.20, latitude: 15.20)
Enter fullscreen mode Exit fullscreen mode
  • Run rails db:seed to seed data

  • Create PlacesController

rails g controller Places index
Enter fullscreen mode Exit fullscreen mode

it will create PlacesController with index action as well as view files for index

  • Update PlacesController to show list of places
class PlacesController < ApplicationController
  def index
    @places = Place.all
  end
end
Enter fullscreen mode Exit fullscreen mode
  • Also, update route.rb to make that index, home page
Rails.application.routes.draw do
  root 'places#index'
end
Enter fullscreen mode Exit fullscreen mode

To test that every thing is working so far, run rails server withbin/rails and go to localhost
You can see default view provided by views/places/index.html.erb

Create basic map with leaflet

  • install leaflet
yarn add leaflet
Enter fullscreen mode Exit fullscreen mode
  • update views/layouts/index.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>LeafletMap</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>

  <body>
    <main class="container mx-auto mt-28 px-5">
      <%= yield %>
    </main>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

We just wrap main content that is <%= yield %> with <main class="container mx-auto mt-28 px-5"> just to give it a little bit style

  • Update views/places/index.html.erb this is place where we will display our map
<div class="w-full">
    <h1 class="font-bold text-4xl mb-2">Places</h1>
     <div data-controller="maps">
        <div data-maps-target="container" class="h-[75vh] w-auto"></div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The main things you need to focus here is data-controller="map" indicates stimulus controller and data-map-target="container" this allow us to define containerTarget on our map stimulus controller

  • Generate map stimulus controller
rails g stimulus map
Enter fullscreen mode Exit fullscreen mode
  • update map_controller.js
import { Controller } from "@hotwired/stimulus"
import L from "leaflet"

export default class extends Controller {
  static targets = ["container"]

  connect() {
    this.createMap()
    this.map.setView([27.700769, 85.300140], 12);
  }

  createMap() {
    this.map = L.map(this.containerTarget)

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

 disconnect() {
    this.map.remove();
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, first we import L from leaflet.when our element with attribute data-controller = "map" enters DOM connect will be called in which we use createMap function to create map.

With line static targets = ["container"] we add container target which we defined in our HTML with data-map-target="container" now that element can be access with this.containerTarget

Inside of createMap() function with line this.map = L.map(this.containerTarget) we initialize empty map inside containerTarget element and we added tile layer to our map with L.tileLayer

After creating map and adding tile, we set map's view to Kathmandu, Nepal with line this.map.setView([27.700769, 85.300140], 12)

  • Add <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin=""/> inside head section of application.html.erb file for styling map
<!DOCTYPE html>
<html>
  <head>
    <title>LeafletMap</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
     integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
     crossorigin=""/>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>

  <body>
    <main class="container mx-auto mt-28 px-5">
      <%= yield %>
    </main>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Now, restart rails server you can see map of Kathmandu,Nepal

map of Kathmandu, Nepal

Showing our list of places on map

  • Update index.html.erb
<div class="w-full">
    <h1 class="font-bold text-4xl mb-2">Places</h1>
     <div data-controller="map" data-map-latlong-value="<%=@places.pluck(:latitude, :longitude)%>">
        <div data-map-target="container" class="h-[75vh] w-auto"></div>
    </div>
</div>

Enter fullscreen mode Exit fullscreen mode

Here, we add data-map-latlong-value="<%=@places.pluck(:latitude, :longitude)%>" which we can access with this.latlongValue from our map_controller.js

  • Update map_controller.js
import { Controller } from "@hotwired/stimulus"
import L from "leaflet"

export default class extends Controller {
  static targets = ["container"]
  static values = { latlong: Array }

  connect() {
    this.createMap()
    this.map.fitBounds(this.latlongValue)
    this.latlongValue.forEach(place => this.addMarker(place))
  }

  createMap() {
    this.map = L.map(this.containerTarget)

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

  addMarker(place) {
    const [latitude, longitude] = place;
    L.marker([latitude, longitude])
      .addTo(this.map)
      .bindPopup(`<div>latitude: ${latitude}</div><div>longitude: ${longitude}</div>`)
  }


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

Enter fullscreen mode Exit fullscreen mode

Line static values = { latlong: Array } add value latlong which datatype will be array and now we can access this value which we defined in index.html.erb with this.latlongValue

Instead of showing map of Kathmandu, Nepal with setView() function we use fitBounds() which take list of latitude and longitude and map will be adjusted to fit all of provided latitude and longitude

And for each latitude and longitude value we call function addMarker. Our addMarker function is responsible for adding marker to provided longitude and latitude and we also add popup showing it's latitude and longitude which will appear while clicking on marker with bindPopup

Now our map is look like this

Map showing list of places

Top comments (0)