I'm just experimenting with Openlayers maps on my site. The map functionality is working great, however I can't get the maps to position on my page correctly.
I need maps to be shown in my search results screen: 1 map per result. The code below is just to show a basic bootstrap card for each result. This displays fine (e.g. a card per result, one on top of the other, as per the screenshot below) until I add the map code. With the map code in, everything overlaps, as per the second screenshot.
Any ideas? Thanks!
Screenshot of results without the map code
Screenshot of results with the map code
<% @locations.each do |location| %>
<div class="card" style="width: 18rem;">
<div id="map" class="map">
<script type="text/javascript">
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([-0.479642,52.641882]),
zoom: 10
})
});
</script>
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</br></br>
<% end %>
That view is pure spagetti. You're missing two closing </div>
tags and you're creating multiple elements with the same ID attribute which gives an invalid document and causes slop mode parsing. My advice is to start using a HTML validator and separate content, behavior and presentation which makes it far easier to write quality code.
When you have multiple elements with the same id attribute document.getElementById
and document.querySelector
will usually return the first element in the DOM but I don't believe this is actually standardized since its prohibited in the first place and the results could vary across browsers.
<% @locations.each do |location| %>
<div class="card">
<div class="map"></div>
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
<% end %>
When using OpenLayers (or Google Maps or Leaflet) you don't actually have to use ids for the target map element - thats just done to create simple quickstart examples which don't really make a great starting point if you want to add multiple maps to a page since you have to jump through a bunch of hoops needlessly just to assign unique ids and then pass them to your JavaScript. Instead you can just pass any HTMLElement object.
Now lets write some real grown up JavaScript:
// place this in your packs (Rails 6) or assets pipeline (older versions)
// use 'DOMContentLoaded' if you are not using turbolinks
document.addEventListener('turbolinks:load', function(){
let elements = document.querySelectorAll('.card .map');
let maps = elements.forEach(function(element){
new ol.Map({
target: element,
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([-0.479642,52.641882]),
zoom: 10
})
});
});
});
This isn't rocket science. We are selecting the elements when turbolinks does its page replacement magic (and on the initial page load) and then initializing a map instance for each element that matches the selector. And you're not sending the exact same JavaScript n number of times for each iteration of the loop like with that script tag abomination.
Use data attributes if you want to pass data like marker positions between Rails and your JavaScript.
<% @locations.each do |location| %>
<div class="card">
<%= content_tag :div,
class: 'map',
data: {
lat: location.latitude,
lon: location.longitude
}
%>
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
<% end %>
document.addEventListener('turbolinks:load', function(){
let elements = document.querySelectorAll('.card .map');
let maps = elements.forEach(function(element){
new ol.Map({
target: element,
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
// Map now centers on right location. Magic!
center: ol.proj.fromLonLat([data.lon, data.lat]),
zoom: 10
})
});
});
});