Mapping Your Rails Journey: A Guide to Leaflet Integration
Tags: webdev, beginners, frontend, ruby
Published: 2024-05-27 14:57:28

Using maps in your application is always the best way to showcase your skill and shine at the moment. Google Maps is one of the best-used maps in any application development, and while it is the best up-to-date map in the market, its only drawback is that it requires a billing payment and an active web URL hosted for it work properly, otherwise, it won't display the map correctly and show a development mode map where there is not much you can do about it. 

There are many other alternatives to Google Maps, some of them are: 

- OpenStreetMap
- Mapbox 
- HERE Technologies 
- TomTom Maps
- Bing Maps
- Leaflet

Leaflet with the OpenStreetMap is a good alternative if you know you're way around tweaking it and rooting out some of the issues of implementing it in ROR, then it is quite a feature to use. How did I discover Leaflet? By total accident really. Like all solutions and code blocks discovered by accident, I once googled an alternative to Google Maps and found Leaflet. 

Simple to implement, totally compatible with all browsers, and no Javascript compiling or load issues where Javascript is not properly loaded.

**Setting up Leaflet with ROR**

Setting up the Leaflet with ROR was straightforward, you start by opening your application.html.erb file, and add the following to the layout.

```
<!-- Leaflet CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
```

We will also be calling the script for the Leaflet right after the yield section, calling the script in the body section is the right place.

```
<%= yield %>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
```

**Setting up your Model**

Since I will be using the Properties model, I will first start by setting some validation for the latitude and longitude in the properties model class

```
validates :latitude, :longitude, presence: true
```

Word of advice, I learned it the hard way, and caused me a lot of pain while implementing the maps. Never use this line if you are using validation

```
attr_accessor :latitude, :longitude 
```

There will always be a conflict between the validates and attr_accessor. The attr_accessor creates getter and setter methods that override the default behavior provided by Active Record, the Rails ORM library. By commenting attr_accessor we will allow the ActiveRecord to use its default methods for accessing and setting these attributes. 

**Setting up the View**

Now let's setup the show.html.erb 

```
<p style="color: green"><%= notice %></p>

<%= render @property %>

<div>
  <%= link_to "Edit this property", edit_property_path(@property) %> |
  <%= link_to "Back to properties", properties_path %>

  <%= button_to "Destroy this property", @property, method: :delete %>
</div>
```

After that we will setup a div container to hold the map and call in the script to retrieve and show the map along with the markers of the location based on the latitude and longitude. So we will set this up in property.html.erb

```
<!-- Include a unique identifier for the map container -->
  <div class="map-container" id="map-<%= property.id %>" style="width: 800px; height: 400px;"></div>

  <script type="text/javascript">
    document.addEventListener("DOMContentLoaded", function() {
      var mapElement = document.getElementById('map-<%= property.id %>');
      var latitude = <%= property.latitude || 37.7749 %>;
      var longitude = <%= property.longitude || -122.4194 %>;

      if (!isNaN(latitude) && !isNaN(longitude)) {
        // Initialize map if it hasn't been initialized yet
        if (!mapElement.classList.contains('map-initialized')) {
          var map = L.map(mapElement).setView([latitude, longitude], 13);

          L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: 'OpenStreetMap'
          }).addTo(map);

          mapElement.classList.add('map-initialized');
        }

        // Add marker for this property
        L.marker([latitude, longitude]).addTo(map)
          .bindPopup('<%= j property.name %>');
      } else {
        mapElement.innerHTML = 'Location not provided for this property.';
      }
    });
  </script>
```
I know what you are thinking, this code could have been written better. Well I got news for you, You are right!!!. Moving on.... 

What the above code is doing is

```
var map = L.map(mapElement).setView([latitude, longitude], 13);
```
This initializes the map using Leaflet, setting the view to the provided latitude and longitude coordinates and a zoom level of 13.

```
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {...}).addTo(map);
```
This adds a tile layer from OpenStreetMap to the map.

```
mapElement.classList.add('map-initialized');
```
This adds a class to the map container to indicate that the map has been initialized. And finally we can add markers to the map with the coordinates and the property name by binding it to the map. 

```
L.marker([latitude, longitude]).addTo(map)
          .bindPopup('<%= j property.name %>');
```

**Adding a marker to the map with latitude and longitude**

This is the interesting part, we get to add some coordinates on the map and then save it the property data, we will be using _form.html.erb to setup the coordinates. 

```
<div>
    <%= form.label :latitude %>
    <%= form.hidden_field :latitude, id: 'latitude', value: @property.latitude || 37.7749 %>
  </div>

  <div>
    <%= form.label :longitude %>
    <%= form.hidden_field :longitude, id: 'longitude', value: @property.longitude || -122.4194 %>
  </div>

  <div style="width: 800px; height: 400px;" id="map"></div>
  
  <script type="text/javascript">
  document.addEventListener("DOMContentLoaded", function() {
    var mapElement = document.getElementById('map');
    var latitude = parseFloat(mapElement.dataset.latitude) || 37.7749; // Default latitude
    var longitude = parseFloat(mapElement.dataset.longitude) || -122.4194; // Default longitude

    var map = L.map('map').setView([latitude, longitude], 13);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: 'OpenStreetMap'
    }).addTo(map);

    var marker = L.marker([latitude, longitude], {draggable: true}).addTo(map)
      .bindPopup('<%= j @property.name %>')
      .openPopup();

    marker.on('dragend', function(event) {
      var lat = marker.getLatLng().lat;
      var lng = marker.getLatLng().lng;
      document.getElementById('latitude').value = lat;
      document.getElementById('longitude').value = lng;
    });

    map.on('click', function(event) {
      var lat = event.latlng.lat;
      var lng = event.latlng.lng;
      marker.setLatLng([lat, lng]);
      document.getElementById('latitude').value = lat;
      document.getElementById('longitude').value = lng;
    });

    // Submit the form when the user clicks a submit button
    var submitButtons = document.querySelectorAll('input[type="submit"], button[type="submit"]');
    submitButtons.forEach(function(button) {
      button.addEventListener('click', function() {
        var form = document.querySelector('form'); // Assuming there's only one form
        form.submit();
      });
    });
  });
</script>
```

Ugh, this code is ugly wish I was at another location than this one. Well lucky for us that is exactly what this code block does.  If the property is getting updated then it is important to load the current latitude and longitude of the property on the map, or else point out to the default location. The below-mentioned event listener is triggered when the marker is dragged and dropped, updating the latitude and longitude values with the new marker position 

```
marker.on('dragend', function(event) {
      var lat = marker.getLatLng().lat;
      var lng = marker.getLatLng().lng;
      document.getElementById('latitude').value = lat;
      document.getElementById('longitude').value = lng;
    });
```

The below-mentioned event listener is triggered when the map is clicked, moving the marker to the clicked location and updating the latitude and longitude values  

```
map.on('click', function(event) {
      var lat = event.latlng.lat;
      var lng = event.latlng.lng;
      marker.setLatLng([lat, lng]);
      document.getElementById('latitude').value = lat;
      document.getElementById('longitude').value = lng;
    });
```

One unusual error I encountered is when submitting the button on the form, where the form would update the property data but not the marker's location on the map or show the new latitude and longitude. To address this issue it is important to add the event listener to the click event on the submit button on the form that would trigger the function to update the coordinates on the map. 

```
var submitButtons = document.querySelectorAll('input[type="submit"], button[type="submit"]');
    submitButtons.forEach(function(button) {
      button.addEventListener('click', function() {
        var form = document.querySelector('form'); // Assuming there's only one form
        form.submit();
      });
    });
```

This is a simple approach to attaching and displaying the latitude and longitude on the map and pointing the marker on the map.