A while ago, my son wanted to sell lemonade by the side of the road. You know how the story goes: we went to the store, got supplies and made the lemonade. Then he and his little sister sat by the side of the road. It was quite a first day, as they pulled in around $90 in lemonade sales! The only downside ... all day long he kept coming inside to make sure his accounting was correct. "Dad, did I charge enough?", "Dad does this look like the correct amount?", "Dad will you go to the store and buy me more lemonade and cups?" At the end of the day the three of us talked through building an application that would help them keep track of their finances. They thought it would be great if other friends could use the application and also keep track of where they put their lemonade stands up, along with the number of sales they made.
The problem
Thinking through the application a few things came to mind. First, within the browser we will need to interact with the Geolocation API to find the latitude and longitude of the device. This browser interaction is asynchronous by nature. Second, this functionality to locate a person should be reusable so it could be utilized in other projects. Third, we need to specify an architecture (e.g. callbacks, messaging or promises) for the main JavaScript code to interact with the modularized geolocation module.
All the examples I’ll show today were written in Visual Studio Code which is a lightweight code-editor for Mac, Linux, or Windows.
Where am I: Geolocation
All modern browsers support the Geolocation API and fetching a device location. The Geolocation API is callback based, meaning as developers we need to pass functions into the API that will be called when the API is done processing. Geolocation is asynchronous, meaning that it could take 100 milliseconds or 100 seconds ... there is no way to know how long it will take. What we do know is that the functions passed in as callbacks will be executed when the browser has finished processing the Geolocation request.
Geolocation is accessed via the window.navigator
object. The geolocation.getCurrentPosition
method takes 3 parameters. For simplicity we will only use the first 2 parameters; a function to be called when the device location is found (i.e. resolveLocation
) and a function to be called when the device location is not accessible (i.e. rejectLocation
).
Notice @ line 10 the resolveLocation
callback function is given a position object from the browser. Among other things this position object contains the needed latitude and longitude properties. They are accessed from the position.coords
object.
The rejectLocation
callback @ line 27 is given an error object from the browser. This error object contains error codes and error messages. The error codes are numeric, ranging from 0 to 3. The 4 different error codes are defined in the ERROR_TYPE_CODES
inferred constant array @ line 30. It should be noted that an error with a type code of 0 or 2 have extra information, hence the errorMessage
string concatenation of error.code
and error.message
.
Reuse: Revealing Module Pattern
In a future article we will talk in-depth about modules. For this conversation we will simply use the Revealing Module Pattern. It will allow us to encapsulate the geolocation interaction within a module that could be used in any project, including whatever you are currently working on.
Let’s unpack this code inside out. The private/inner getLocation
function @ line 12 will be the function that performs the geolocation lookup with browser. This private function is exposed via the Module API (i.e. the return block @ line 17). The return block is simply a JavaScript object for specifying the relationship between public methods and the private methods they hide.
Notice the inner getLocation
and return
block are wrapped in an Immediately Invoked Function Expression (i.e. IIFE). Most importantly an IIFE has a function that the browser invokes after it has been parsed. This invoking occurs via the () @ line 21. Secondly, a closure is formed due to the Module API being returned from the IIFE. This closure allows the state of the module (i.e. the function’s execution context) to live for the length of the entire application! This idea seems singleton-esque, as this module is only created once; however this is not the singleton pattern. Revealing modules are instantiated immediately after being parsed, whereas singletons are not instantiated until first use (e.g. their getInstance
is invoked).
The guts of the revealing module pattern allows for the encapsulation of private variables that can only be accessed and changed via publicly defined getters and setters. In this location module the inner/private getLocation
function is not directly accessible. Only via a call to the public DI.location.getLocation
method will provide the ability to call the inner/private getLocation function scoped to the anonymous function.
Lastly, we create a namespace @ line 7. Within DevelopIntelligence we use DI as our namespace object. This allows all DevelopIntelligence code to be stored on only one object attached to the global JavaScript window object, minimizing the pollution of the global object.
The main.js JavaScript file calls the location module’s public getLocation
method via the DI namespace which in turn executes the private getLocation
method functionality.
Modularize the Geolocation
So far we have seen how to utilize geolocation within the browser. We have also seen how to write a module, via the Revealing Module Pattern. Let’s put those together so we have an actual modularized Geolocation interaction.
In the code above the call to the Geolocation API has been inserted into the private/inner getLocation
function @ line 9. Building a module in this fashion abstracts away the implementation details of dealing with Geolocation. It also allows the location-module.js file to be used in any project without ever having to duplicate those implementation details of the resolveLocation and rejectLocation function.
Communication: Speak module, speak!
We have one glaring probl
em with this setup! We haven’t dealt with the asynchronous nature of geolocation in respect to module interaction. When the main.js file calls the geolocation module we have not defined a way to pass back the longitude and latitude properties out of the module and back into the main calling code. At this point we are only able to tell the location module to do work, but have no way of knowing when it completes to interact with the data. Let's look at a few different ways to handle this architecturally: first callbacks, then messaging and finally promises.
Callbacks ...
Callbacks provide a guaranteed call and response between the main calling code and the geolocation module. However, that guaranteed interaction comes at the price of being tightly-coupled.
We have seen a callback architecture in the wild when we utilized the Geolocation API. Remember, when we called the window.navigator.geolocation.getCurrentPosition
function we had to pass in functions that would be called when the API was done processing its asynchronous code. This was tightly-coupled because the calling code needed to know the implementation details of how to handle the position object that was handed back to the successful callback and the implementation details of how to handle the error codes and messages handed back to the failure callback.
In the code above @ line 18 our main JavaScript functionality passes a callback function reference to the module. The module will execute this callback when the module’s asynchronous code has completed. The beauty of a callback based architecture comes in the guarantee that the passed in callback function reference will be executed when the module deems necessary.
The location module no longer simply prints the position coordinates via the console. Rather @ line 44 the module executes the successCallback
callback function reference that was passed into the getLocation
function signature. Remember the function that is actually going to be called is the injectCoordinates
located in the index.js JavaScript file. When the injectCoordinates
function is executed the longitude and latitude will be injected into the HTML.
Callbacks give a guaranteed communication architecture between the main JavaScript and the called module. However, this architecture comes at a cost in terms of readability, understandability and coupling as the main JavaScript hands application control over to the module to run a callback at a future point in time.
Messaging ...
Messaging provides us a loosely-coupled architecture allowing all of the implementation details to be hidden within the module. However, this loosely-coupled architecture comes with the price of having no guarantee of response from the module to the main calling code.
Within a messaging architecture the main calling code only needs to know how to ask for data from the module and how to listen for a response. After the asynchronous code is run within the module an event is fired, containing the data, allowing all listening code a chance at interacting with the data.
Custom JavaScript events are utilized to create a messaging architecture. To facilitate a looser coupling the location module will expose the type of Custom Event the module has created. @ line 23 the main calling code stores the custom event type and then @ line 26 it registers an event listener for that specific event type.
When the location module finishes its asynchronous code it will fire the custom event and the registered event listener's callback function will be called. On a successful interaction @ line 28-29 the injectCoordinates
function will be called and insert the longitude and latitude into the HTML.
As mentioned above, the messaging based location module will expose a Custom Event Type via the getEventType
function along with the location via the getLocation
function. This allows the location module to change event types without having to change any code in the main JavaScript file. In other words the code calling this module doesn’t need to know it is a custom event of type location.
The location module will create a new custom event that passes the data retrieved from the geolocation interaction. It utilizes the CustomEvent constructor with an event type (i.e. location) and a message object set to the detail property @ line 66. resolveLocation the successful callback creates a message object containing the coordinates.latitude
and coordinates.longitude
properties along with a boolean success property set to true.
After the custom event has been created the module then fires/dispatches the event. @ line 67 the dispatchEvent
method is invoked via the document.body
element to fire the created custom event. Once the event is fired the browser runs the event handler callback function previously registered within the main JavaScript file, which in turn inserts the latitude and longitude into the HTML.
Promises
Promises provide us the benefits of the callback and messaging architecture. They give a guaranteed delivery of data, while not forcing the knowledge of implementation details. After the modules asynchronous code is run the data is handed back to the calling code via a predefined API.
A promise based module differs from callback based module in that it never controls the application. The calling code asks the promise module to do something and when it’s done it hands its collected data back to the calling code. The promise module isn’t in charge of invoking a method back on the calling code. This means it never needs to hold onto a reference for a specific function defined within the main calling code, allowing for a more loosely-coupled architecture than a callback based architecture.
A promise based module differs from messaging based modules in that the promise architecture guarantees a response to the calling object. The calling code asks the promise module to do something and immediately a transactional receipt (i.e. a promise object) is handed back to the calling code. On the transactional receipt the calling code sets up a callback function that will be run when the promise module is done. This means there is no guessing or hoping that the calling code has registered the correct event listener type to the correct DOM element, guaranteeing that the calling code will get the data needed when it is ready. Also, the promise architecture gives a pre-defined, consistent API taking away the guesswork of handling the return of asynchronous data from a modules data.
There are many different frameworks that utilize a promise based architecture. jQuery utilizes a Deferred Object, Kris Kowal created a q library, Angular utilizes a form of that library in the $q service, and now ES6/ES2015 has promises baked in.
Above we are interacting with ES6/ES2015 promises. To see the others in action hit up our DevelopIntelligence GitHub Repo on Promises. Notice @ line 18 in the calling code a call is made to the DI.currentLocation
module. As I previously stated, immediately a transactional receipt is handed back, (i.e. a promise object). This promise object exposes standard API methods for the calling code to interact with.
The most often used method in the promise API is the .then method. It is supported across all promise framework implementations. Within the .then
method we specify functions that will be run when the DI.currentLocation
module is done processing its asynchronous code. In this case when the browser resolves a latitude and a longitude, the DI.currentLocation
module hands that data back as an object to the calling code via a parameter in the .then
method. As a side-note, interacting with that data object returned within the .then
method can be referred to as "unwrapping the promise."
It's time we saw how to create promises instead of callbacks or messaging. With the new ES6 / ES2015 Promise Object baked in, we create a promise object instance from the Promise
constructor via the new operator as can be seen @ line 31. Within the Promise constructor we define an anonymous function receiving a resolve
and reject
callback method.
Think back to the callback architecture example above. Within the promise module (i.e. the geolocation module) there were similar callback methods which were defined as successCallback
and failureCallback
. The resolve
and reject
callbacks are to be called when the asynchronous data is available for interaction. In other words with the browser's gets its current position, the resolve
method is called and the coordinate data will be sent out of the module. At that point the .then method that was defined within the calling script will be triggered and the location data will be processed and placed on the browser.
The Takeaway
Interacting with asynchronous data introduces complexity into an application. When interacting with a device's geolocation, we as developers, are at the mercy of when that data will be given to our application for processing. Promises give us a convenient, pre-defined API to manage the asynchronous complexity. They provide a way for developers to have the loose-coupling of a messaging architecture and the guarantee of a callback architecture.
This article is part of the web development series from Microsoft tech evangelists, engineers and DevelopIntelligence on practical JavaScript learning, open source projects, and interoperability best practices including Microsoft Edge browser. DevelopIntelligence offers instructor-led JavaScript Training and React Training for technical teams and organizations.
We encourage you to test across browsers and devices including Microsoft Edge – the default browser for Windows 10 – with free tools on dev.microsoftedge.com, including Internet-scale data, a powerful tool to understand what properties are important to the real web, and what APIs are interoperable across browsers. Also, visit the Edge blog to stay updated and informed from Microsoft developers and experts.