Using Javascript with Ruby
During our last project, in which we created an application that tracked the distances for takeaway shops in a certain radius to a user's location, a key part of the interface was embedded maps.
We embedded maps for the user's location, for the location of each takeaway, and even for where certain dishes in our database came from. As such, a key part of our project was learning to use the Google Maps API, which was written in Javascript.
Anatomy of a Map
This is the structure of the code block that contains our map. It is written into a view file, wrapped in HTML. First, we need to define the area where we intend to put our map. The Javascript code follows afterward, in <script> tags. However, it doesn't have to go here. I put it here for clarity, but in fact it would perhaps be best placed at the bottom of the <body> tag. There are two separate Javascript tags here. The first one creates the map element. The second one calls the API - or links the files/resources needed to create the map, as well as providing our authentication details.
<div class="col-md-6">
<!--Below is the div element containing the map. We have tagged it with an id of "map-container", which is what we are going to use later to let the mapknow where it should go. -->
<div class="container" id="map-container">
</div>
<!-- And here, we inject our script tag with the Javascript inside. Now, knowing more about Javascript we know that this should perhaps go at the bottom of the <body> -->
<script>
//Insert content here from the next code block...
</script>
<!-- Under the first Javascript tag, here we call the google maps API, which contains our key. For security purposes we have hidden it. -->
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=..............................&callback=initMap">
</script>
</div>
You'll find the second script also has an 'async defer' in its tag. What does this mean? Well unlike our first script tag, it specifies that the script is executed when the page has finished parsing. The defer attribute is only for external scripts (should only be used if the src attribute is present). When an external script has this attribute, the file can be downloaded while the HTML document is still parsing. Once it has been downloaded, the parsing is paused for the script to be executed.
That means that the call to the API (you'll see in the URL where we specify we want the init function) will be downloading while our page is loading, and finally when our page is loaded it will fire the call. This makes the loading of the page efficient, and makes sure that all of the elements on the page will be rendered before we try to make changes to any of them.
Mix and Match
OK, what about this initMap function?
Here is the code that was inserted inside our first <script> tag above.
In the absence of wanting to create a whole other Javascript file to deal with our maps, and given the occasional need to use the information from Ruby variables within our maps (specifically, the latitude and longitude variables that were stored within our database) we often ended up mixing Ruby and Javascript, and seeing how far we could push that kind of behaviour.
function initMap() {
// We define a variable 'place', which is a hash containing the coordinates of the location of the marker we want to put on the map. As you can see these are provided by Ruby variables. Using the familiar Ruby tag, we are able to insert those values into our Javascript variable.
var place = {lat: <%= @restaurant.get_coordinates[0] %> , lng: <%= @restaurant.get_coordinates[1] %>};
//We create a new map, which calls from the Google Maps API, and plug it the div element we defined above.
var map = new google.maps.Map(document.getElementById('map-container'), {zoom: 12, center: place});
//We create the actual marker, and put it at the position of the place variable we defined earlier.
var marker = new google.maps.Marker({position: place, map: map});
}
This surprisingly worked. What about this?
//Here, I used Ruby to create an array of restaurant coordinates and restaurant information (name, ID, and address) that I'm later gonna stick into an InfoWindow (a Google Maps feature that will pop up a little info box when I click on the marker).
var locations = <%= raw @restaurants.map {|restaurant| [restaurant.name, restaurant.getcoordinates[0], restaurant.getcoordinates[1], restaurant.id, restaurant.address]} %>;
//Initializing the map....
function initMap() {
//Creating new map (var map) ...
var map = new google.maps.Map(document.getElementById('all-restaurants-map-container'), {
zoom: 12,
center: new google.maps.LatLng(51.5138, -0.15),
mapTypeId: google.maps.MapTypeId.ROADMAP
});
//Creating the new InfoWindow and Marker...
var infowindow = new google.maps.InfoWindow()
var marker, i;
//And I iterate through my locations variable to produce a new marker and info window for every entry.
for (i = 0; i < locations.length; i++) {
marker = new google.maps.Marker({
position: new google.maps.LatLng(locations[i][1], locations[i][2]),
map: map
});
google.maps.event.addListener(marker, 'click', (function (marker, i) {
//Here I interpolate HTML into Javascript to be able to insert the information from my array into the Window. And you will notice that I use the ${} tag, to call Javascript variables within the HTML. That's a lot of mixing and matching.
return function () {
var contentString = '<div id="content">'+
'<div id="siteNotice">'+
'</div>'+
'<h5 id="firstHeading" class="firstHeading">'+locations[i][0]+'</h5>'+
'<div id="bodyContent">'+
`<p> <a href="/restaurants/${locations[i][3]}">
${locations[i][4]}</a>`+
'</div>'+
'</div>';
infowindow.setContent(contentString);
infowindow.open(map, marker);
}
})(marker, i));
}
}
Is this best practice?
To be honest with you, no. While it's a quick fix for a project like this one, and probably was a lot more common before, according to seasoned Ruby/JS developers, it's on it's way out.
The main problem here is that it couples your JS too tightly to your view HTML, which causes a lack of flexibility and too much complexity. This is not code that can be recycled. Also, any changes or updates that you make would have to go through the HTML.
However, in this particular project, it does allow us a degree of flexibility in being able to access variables that may be constantly updated to populate a few maps. For a larger project this wouldn't be a good idea, but it fit ours.