Setting up VueJS

Developing with Javascript has become very sophisticated. In fact, you will find lot of resemblance on the set of tools that we will use with Javascript and tools used with other languages and frameworks. In fact, Phoenix integrates very well with modern Javascript development tools, and we have already been exposed to them without even noticing.

We will get to know such tools as we progress with the material on this session. Let us first add VueJS to our project. To that end, run the following command (in a terminal window), within the project’s folder assets/.

npm install --save vue

npm stands for node package manager and is the tool that we will use. In fact, npm is a very versatile tool that serves, not only to manage the dependencies of a javascript project, but also as powerful tool for executing other tasks in our development workflow.

Note that we are requesting the installation of the library vue, which is fetched from a globally accessible web registry. Moreover, the option --save specifies that we would like to add the dependency on the projects file package.json. Take a look at this file. It contains all the project’s javascript dependencies. Do not forget to add the option --save when you install a dependency, particularly if you are working in a team as it is the only way other team members get to know about the dependencies that you have added to the project.

npm is a general purpose tool that works fine, not only with front-end but also with backend applications (e.g. backend applications using ExpressJS). In our context, we will also rely on one additional tool, which bundles the javascript code (e.g. application code + libraries) in a way that it can be properly dispatched to the browser. Examples of such tools are Browserify and Webpack. Interestingly Phoenix team decided not to use any of the two previously mentioned tools but another one called BruchJS. Before continuing, we would need to configure brunchjs to serve VueJS. Open the file brunch-config.js and replace the configuration connected with npm with the following snippet:

exports.config = {
  ...
  npm: {
    enabled: true,
    aliases: { vue: "vue/dist/vue.common.js" }
  }
}

Now, that we are properly configured VueJS, let us write our first working example.

Open the file called assets/js/app.js and replace its content with the following code.

import Vue from "vue";

new Vue({
  el: '#takso-app',
  data: {
    message: 'Hello there!'
  }
});

The code above imports Vue from the vue, which corresponds to the vue library that we added to the list of project’s javascript dependencies. Then, we initialize Vue with the configuration object that specifies the following:

  • Vue must look for an HTML element with identifier takso-app on which Vue will dynamically append all the HTML elements that are handled by Vue.
  • There is a data variable called message that is initialized with the value Hello there and that will be accessible from HTML templates.

Let us now add one the aforementioned HTML templates. Open the file lib/takso_web/templates/page/index.html.eex and replace the whole content of this file with following snippet (do not be worried about the radical change … it seems a lot of HTML thrown to the garbage, but that HTML is just the welcome message that Phoenix generated from the very beginning).

<div id="takso-app">
  {{ message }}
</div>

As you can see, the identifier associated with the div is takso-app, which matches with the configuration object that we used to initialize Vue. Moreover, there is a reference to message from inside a handlebar tag (the tag formed with two curly brackets).

Run your application and open the root page of it, i.e. navigate to the URL http://localhost:4000. If everything has been properly set, you would see a Hello there in the application’s root page.

Double-way data binding

With the previous example you have already seen the power of Vue’s data binding. Vue will automatically render the value of a data variable defined in Vue’s configuration object in the places we desire by just referring to the data variable from inside a handlebar tag. If you have worked with libraries such as jQuery you will agree that this handlebar tag notation is very handy, as you avoid a lot a boilerplate code (e.g. selecting an HTML element and then changing the corresponding value as required). However, in some cases, we need communication in the other way around (reading a value in the template and propagating that value to the javascript code). Let us see how VueJS accomplish this other way of data binding.

<div id="takso-app">
  <input type="text" v-model="message">
  {{ message }}
</div>

As you can see, we have just added a humble HTML input element. However, this element carries a v-model attribute which comes from Vue. Such attribute specifies that the data variable message will be set by the input element. Note that the binding is two-way: the input text will be initialized with the string Hello there from the javascript code and can be later be changed with the input element!

Handling booking requests

Let us now try to connect the Javascript frontend with our Phoenix-based backend. To illustrate the procedure we will start by implementing the booking request process.

Hence, we will replace our toy HTML template by something more realistic. Take the following HTML template and replace with it the content of the file lib/takso_web/templates/page/index.html.eex.

<div id="takso-app">
  <div class="form-group">
    <label class="control-label col-sm-3" for="pickup_address">Pickup address:</label>
    <div class="col-sm-9">
      <input type="text" class="form-control" id="pickup_address" v-model="pickup_address">
    </div>
  </div>
  <div class="form-group">
    <label class="control-label col-sm-3" for="dropoff_address">Drop off address:</label>
    <div class="col-sm-9">
      <input type="text" class="form-control" id="dropoff_address" v-model="dropoff_address">
    </div>
  </div>
  <div class="form-group"> 
    <div class="col-sm-offset-3 col-sm-9">
      <button class="btn btn-default" v-on:click="submitBookingRequest">Submit</button>
    </div>
  </div>
</div>

Please note that I deliberately removed the typical HTML form tag from the template. The sole presence of such tag triggers an interaction that will result in reloading the web page, behavior that is not desirable in our context. Instead, we will program the interactions with the backend by means of Javascript functions.

In the template above, you can easily find that we have defined two input elements, which are bound to vue data variables (i.e. one for the pickup address and the other one for the drop off address). The effect of this data binding should already clear to you by now.

The new element in this template is the way we capture the click events on the submission button. As you can see, Vue provides an HTML attribute to this end. Indeed, the attribute v-on:click="submitBookingRequest" implies that on the occurrence of a click over the submission button, Vue will trigger the execution of the vue method submitBookingRequest (with zero parameters).

The code below completes our first version of the Javascript code.

import Vue from "vue";

new Vue({
  el: '#takso-app',
  data: {
    pickup_address: "Liivi 2",
    dropoff_address: ""
  },
  methods: {
    submitBookingRequest: function() {
      console.log(this.pickup_address, this.dropoff_address);
    }
  }
});

As you can see, the Vue initialization provides a section for defining methods. In this moment, we would just print out to the console the current values of pickup and dropoff address as captured within the corresponding text input elements.

Now, at this point we need to figure out how to interact with the backend to forward the information to starting the booking request. It is important to understand that, from this session, we are working with two complementary applications: the Phoenix-based backend application and the Javascript-based front end application. In this first session, we will use an HTTP-based interaction: the backend must provide a REST API and the front-end consumes the services provided therein.

There are several Javascript libraries for implementing the HTTP-based interactions. That includes the low-level AJAX XMLHttpRequest, which is too low level and not necessarily portable. For this reason, I select another library called axios. Add the library with the following command.

npm install --save axios

Let us update the Javascript code to already include the interaction with the backend. Copy then the following code where it belongs.

import Vue from "vue";
import axios from "axios";

new Vue({
  ...
  methods: {
    submitBookingRequest: function() {
      axios.post("/api/bookings", {pickup_address: this.pickup_address, 
          dropoff_address: this.dropoff_address})
        .then(response => console.log(response))
        .catch(error => console.log(error));
    }
  }
});

Well, as you can see, the URL that we are using is a new one. Although we can use the module BookingController to hold the code for the REST API, I prefer to keep the HTML-based code and the REST API in separated modules. So, let us add the support in the backend.

Let us first add the route. To that end, I will ask you to add a new context (the context skeleton is currently commented in our code). Please note that the scope uses a very different pipeline (sequence of plugs) to the one used by HTML-based interactions.

  scope "/api", Takso do
    pipe_through :api
    post "/bookings", BookingAPIController, :create
  end

Now, copy the following code into the file web/controllers/booking_api_controller.ex.

defmodule Takso.BookingAPIController do
  use Takso.Web, :controller
  import Ecto.Query, only: [from: 2]
  alias Takso.{Repo,Taxi}

  def create(conn, _params) do
    available_taxis = Repo.all(from t in Taxi, where: t.status == "available", select: t)
    if length(available_taxis) > 0 do
      taxi = List.first(available_taxis)
      put_status(conn, 201)
      |> json(%{msg: "Your taxi will arrive in 5 mins", taxi_location: taxi.location <>", Tartu, Estonia"})
    else
      put_status(conn, 409)
      |> json(%{msg: "Booking request cannot be served"})
    end
  end
end

You should understand most of the code above. The only difference is, probably, the use of the function json/2 that takes the connection and a map to produce a JSON body, that is the serialized version of the map.

Restart your application and analyze the behavior of it in your browser.

Playing with Google maps geolocation APIs

The HTML 5 specification introduced a set of convenience functions to query the current location of the computer. Note that this feature does not rely only on specialized hardware, such as GPS cards, but on other means such as the wire and wireless network infrastructure. That said, let us add this capability into our application.

Adapt your file web/static/js/app.js so as to include the mounted block, as shown below.

new Vue({
  ...
  methods: {
    ...
  },
  mounted: function () {
    navigator.geolocation.getCurrentPosition(position => {
      console.log(position.coords);
    });
  }
});

The mounted block corresponds to code that is executed immediately after Vue has completed the preprocessing of the underlying HTML template. As you can see, we call the function naviagor.geolocation.getCurrentPosition to retrieve the current geolocation. However, you will also notice that we do not wait for the corresponding value, but provide a callback function (the innner anonymous function) which will be executed once the computer determines the value to be returned. This is a very typical pattern: Javascript uses callback functions to free the single thread that the language provides to avoid blocking the computation. Blocking the computation would make the browser freeze for a little moment, which is undesirable.

With the information that we retrieved we are going to help the end-user. Indeed, we can use the current location to specify the pickup address for a taxi booking. However, the numbers are not meaningful to the end user. Instead, we will use another service to translate the coordinates into the corresponding address. To this end, we will use Google maps geocoding service.

Google maps services impose a limit on the number of requests that a user can perform in certain periods of time. To avoid a problem, I propose you to get an API Key and configure your application to use such key.

First, you have add a script tag that downloads the Google maps javascript libraries to your HTML templates. In fact, we have add the script tag to only one of the templates, the one contained in file lib/takso_web/templates/layout/app.html.eex, as this is the root template for our application. Add the following tag to that file by the end of the file (just before the tag that loads the script file app.js).

<script src="//maps.googleapis.com/maps/api/js?key=<%= Application.get_env(:takso, :gmaps_api_key) %>"></script>

As you can see, the tag refers to a sort of environment variable called :gmaps_api_key. Such environment variable should be defined in the one of the configuration files. One good place to put it is the file config/prod.secret.exs, but such file is only loaded in production mode. For the sake of brevity, during the live session, I added it into the file config/config.exs. However, you could also add another file, say config/dev_test.secret.exs, which you can also add to the .gitignore file to avoid sharing your key with others. In any case, the idea is that you have to add a line like the following one to the configuration file.

config :takso, gmaps_api_key: "your google maps api key"

With that configuration step completed, we can start using the Google maps API. As a first example, we are going to translate the coordinates returned by the HTML geolocation service into a textual address. Copy the code below to where it belongs:

new Vue({
  ...
  methods: {
    ...
  },
  mounted: function () {
    navigator.geolocation.getCurrentPosition(position => {
      let loc = {lat: position.coords.latitude, lng: position.coords.longitude};
      var geocoder = new google.maps.Geocoder;
      geocoder.geocode({location: loc}, (results, status) => {
          if (status === "OK" && results[0])
            this.pickup_address = results[0].formatted_address;
        });
    });
  }
});

As you can see, we are initializing a variable loc with the information about the latitude and longitude returned by the HTML 5 geolocation service. Unfortunately, the data structure used by the HTML 5 specification is not compatible with the one used by Google maps API. After that, we instantiate the geocoding service and call the geocode to determine the information associated with the coordinates provided as a parameter. The geocode function requires a callback function to return the result. Note that if the coordinates (or address as we will see later) are invalid, the callback function will be called with a status of “ERROR” that is why, we are checking that the status is “OK” and that the variable results holds an array with at least one element. Note that the array results contains the information associated with the input coordinates at several levels of granularity. It is the information stored in index 0 the one that is useful for our purposes. So, we retrieve the value of results[0].formatted_address and store it into the data variable this.pickup_address. When you execute the code, you will see that the result of this is that the text field associated with pickup address will display one actual address, which is the closest to our location.

Another service provided by Google maps API is the rendering of maps. With the current setup, we will need to add just a couple of javascript lines to requesting the rendering of a map, centered on our current position.

Add first the snippet <div id="map" style="width:100%;height:300px"></div> to the template web/templates/page/index.html.eex just after the form, but still inside the div that is under the control of VueJS. Then, update your code (i.e. assets/js/app.js) according to the snippet shown below.

new Vue({
  data: {
    ...
    map: null,
    geocoder: null
  },
  methods: {
    ...
  },
  mounted: function () {
    navigator.geolocation.getCurrentPosition(position => {
      let loc = {lat: position.coords.latitude, lng: position.coords.longitude};
      this.geocoder = new google.maps.Geocoder;
      this.geocoder.geocode({location: loc}, (results, status) => {
          if (status === "OK" && results[0])
            this.pickup_address = results[0].formatted_address;
        });
      this.map = new google.maps.Map(document.getElementById('map'), {zoom: 14, center: loc});
      new google.maps.Marker({position: loc, map: this.map, title: "Pickup address"});
    });
  }
});

In the code above, we instantiate a Google map, with an anchor on the div that we just added (the one that has identifier map), and we set the zoom level 14 and is centered with respect to our current location (i.e. the variable loc has been initialized with such an information). In addition to the map, the code above illustrates how we can added a marker indicating our current location. Note that I have two data variables to hold the references to the geocoder and to the map, respectively. We will use such variables to render some additional information, namely, we would add a marker to the taxi’s current location. Please update the implementation of the function submitBookingRequest according to the snippet shown below.

new Vue({
  ...
  methods: {
    submitBookingRequest: function() {
      axios.post("/api/bookings", {pickup_address: this.pickup_address, 
          dropoff_address: this.dropoff_address})
        .then(response => {
          this.geocoder.geocode({address: response.data.taxi_location}, (results, status) => {
            if (status === "OK" && results[0]) {
              var taxi_location = results[0].geometry.location;
              new google.maps.Marker({position: taxi_location, map: this.map, title: "Taxi"});
            }
          });
        })
        .catch(error => console.log(error));
    }
  },
  ...
});

With this, we complete our first iteration of STRS’s frontend application.