Implement Leaflet on rails with stimulus
Tags: rails, hotwire, webdev, ruby
Published: 2023-05-07 04:12:45

## 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.

```shell
rails new leaflet_map -T -d postgresql --css=tailwind --javascript=esbuild
cd leaflet_map
rails db:create
```

- Configure Tailwind: 
In `application.tailwind.css` replace the `@tailwind `directives with `@import`

```css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```
## Create basic MVC
- create `Place` model which will have longitude, latitude and name attributes

```shell
rails g model Place name:string longitude:float latitude:float
rails db:migrate
```
- add simple validation for name, longitude and latitude on `Place` model

```ruby
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.rb` file to create some data for places

```ruby
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:seed` to seed data

- Create `PlacesController`

```shell
rails g controller Places index
```
it will create `PlacesController` with index action as well as view files for index
- Update `PlacesController` to show list of places

```ruby
class PlacesController < ApplicationController
  def index
    @places = Place.all
  end
end
```
- Also, update `route.rb` to make that index, home page

```ruby
Rails.application.routes.draw do
  root 'places#index'
end
```
To test that every thing is working so far, run rails server with`bin/rails` and go to [localhost](http://127.0.0.1:3000/) 
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`

```html
<!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.erb` this is place where we will display our map

```html
<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`

```javascript
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();
  }
}
```
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

```html
<!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

![map of Kathmandu, Nepal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qsiftf2h7g5d5rr1w216.png)

## Showing our list of places on map

- Update `index.html.erb`

```html
<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`

```javascript
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();
  }
}

```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gvjmkfee70qv5wj0asl7.png)