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
- Configure Tailwind:
In
application.tailwind.cssreplace the@tailwinddirectives with@import
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
Create basic MVC
- create
Placemodel which will have longitude, latitude and name attributes
rails g model Place name:string longitude:float latitude:float
rails db:migrate
- add simple validation for name, longitude and latitude on
Placemodel
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
- update
seed.rbfile 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)
Run
rails db:seedto seed dataCreate
PlacesController
rails g controller Places index
it will create PlacesController with index action as well as view files for index
- Update
PlacesControllerto show list of places
class PlacesController < ApplicationController
def index
@places = Place.all
end
end
- Also, update
route.rbto make that index, home page
Rails.application.routes.draw do
root 'places#index'
end
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
- 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>
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.erbthis 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>
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
- 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: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map);
}
disconnect() {
this.map.remove();
}
}
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 ofapplication.html.erbfile 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>
Now, restart rails server you can see 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>
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: '© <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();
}
}
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


Top comments (0)