AngularJS filters are awesome for modifying the output of data as it is being displayed in the view and Angular comes with many of these filters built-in with the framework. For example, with the currency
filter we can easily display numeric values with a dollar sign and two decimal places and with date
we can format dates and times however we like. The built-in filters are great, but what if we want to make a custom filter that goes beyond these? For this we can make use of AngularJS custom filters.
Most AngualrJS custom filter examples that we see around the web deal with fairly straightforward use cases, such as the one in the AngularJS custom filter docs that checks whether a value is present and returns either a checkmark or an X.
In this tutorial we will build a more complex AngularJS custom filter that will allow us to modify geographic coordinates in our views and also make use of the filter in a controller. The filter will accept geographic coordinates in degrees, minutes, and seconds notation, or in decimal degrees, and will return the conversion for us.
Quick note: I’m writing a book called Securing Angular Applications. It will teach you everything you need to know to about authentication, authorization, and security so you can properly lock down your Angular apps. Check it out if you’re interested :)
Getting Started
To get a good feel for how AngularJS custom filters can be used, we will build out a small application that will incorporate an AngularJS custom filter with a Google Map to display geographic coordinates. The filter will convert the geographic coordinates from degrees, minutes and seconds notation to decimal degress notation and vice versa.
Different Types of Geographic Coordinates
You’re probably most familiar with geographic coordinates being presented in the typical degrees, minutes and seconds (DMS) notation, for example 62°26′32″N 114°23′51″W. This coordinate pair gives us latitude and longitude values that represent a single point on the earth. While this notation is common, it is cumbersome to use in many cases, and thus an alternative exists—decimal degrees (DD).
To convert from DMS to DD, we do a bit of math on the values.
- Keep the degree value (62 and 114 above)
- Add the minute value (denoted by ′) divided by 60 (26/60 and 23/60 above)
- Add the second value (denoted by ″) divided by 3600 (32/3600 and 51/3600 above)
Thus, our coordinate pair above in DD notation is 62.4422N and 114.3975W. By convention, we use the negative sign to signify any coordinate that is in the Western or Southern hemisphere, which allows us to drop the N and W above. The final coordinate is 62.4422 and -114.3975.
Creating a New Angular Project
Let’s start by creating a new directory called angular-coordinates
and installing AngularJS, Angular Google Maps, Angular Sanitize and Bootstrap. Assuming you have Bower installed, from the command line:
bower install angular angular-google-maps angular-sanitize bootstrap
Next let’s create an index.html
file in our root directory and setup its basic structure:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Angular Coordinates</title>
<link rel="stylesheet" type="text/css" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body ng-app="angular-coordinates" ng-controller="mapController">
<div id="map_canvas">
<ui-gmap-google-map center="map.center" zoom="map.zoom" options="options">
<ui-gmap-marker
coords="marker.coords"
options="marker.options"
events="marker.events"
idkey="marker.id">
</ui-gmap-marker>
</ui-gmap-google-map>
</div>
<div class="col-sm-6 col-sm-offset-3 coordinates-form">
<div class="col-sm-6">
<input
type="text"
ng-model="lat"
class="form-control coordinate-input"
placeholder="Enter Latitude"
/>
</div>
<div class="col-sm-6">
<input
type="text"
ng-model="lon"
class="form-control coordinate-input"
placeholder="Enter Longitude"
/>
</div>
</div>
<div class="info-box">
</div>
</body>
<!-- Application Dependencies -->
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="bower_components/lodash/dist/lodash.js"></script>
<script src="bower_components/angular-google-maps/dist/angular-google-maps.js"></script>
<!-- Application Scripts -->
<script type='text/javascript' src='scripts/app.js'></script>
<script type='text/javascript' src='scripts/mapCtrl.js'></script>
<script type='text/javascript' src='scripts/coordinateFilter.js'></script>
<script type='text/javascript' src='scripts/functions.js'></script>
</html>
Our HTML document loads the dependencies we pulled in with Bower and has a basic page structure setup. The #map_canvas
div is where the Angular Google Maps map goes and as you can see, we’re working with a Google Map and a Google Map Marker. We’ve passed in some options to the directives, like the centering coordinates for the map and for the marker. These coordinates and the other items you see in the Angular Google Maps directive are on $scope
and are initialized in the controller which we will see below.
We’ve also included input boxes where we can type out the coordinate pair. Once we have everything wired up correctly, the map will pan to update it’s center and marker position as the user types coordinates in.
Finally you’ll see that there are a bunch of appication scripts that we’re pulling in that we haven’t created yet. We’ll do that below.
Creating Custom Styles
We’ll need some custom styles for our app to make it look a bit better that what we get out of the box with Bootstrap.
/* css/style.css */
.coordinate-input {
height: 75px;
font-size: 30px;
}
html, body, #map_canvas {
height: 100%;
width: 100%;
margin: 0px;
}
#map_canvas {
position: relative;
}
.angular-google-map-container {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
.coordinates-form {
position: absolute;
top: 25px;
}
.info-box {
position: absolute;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.7);
height: 200px;
}
h1, h2, h3, h4, h5, h6, p {
color: #ffffff;
}
Create the app.js
File
Let’s create our main Angular application file, app.js
in the scripts folder. In this file we will declare the Angular application module and load the dependencies we need.
// scripts/app.js
(function() {
'use strict';
angular
.module('angular-coordinates', ['uiGmapgoogle-maps', 'ngSanitize']);
})();
You can see here that we’re matching up the module name, angular-coordinates
with what we have declared in the ng-app
directive on the body tag. We’re loading two dependencies for this project—Angular Google Maps which will be used to render the background map, and Angular Sanitize which will be used to bind values from the filter directly to HTML.
Creating the Map Controller
Since we’re using Angular Google Maps for this project, we’ll need a simple controller that will mostly be used for communicating coordinate information to the map directive. Let’s create a file called mapController.js
in the scripts folder:
// scripts/mapController.js
(function() {
'use strict';
angular
.module('angular-coordinates')
.controller('mapController', map);
function map($scope) {
$scope.map = {
center: {
latitude: $scope.lat || 62.4568,
longitude: $scope.lon || -114.3964
},
zoom: 7
}
$scope.marker = {
id: 0,
coords: {
latitude: $scope.lat || 62.4568,
longitude: $scope.lon || -114.3964
},
options: {
draggable: false
}
}
}
})();
The objects we’ve put on $scope
here in the controller will allow us to initialize the Angular Google Map when the page loads. We have two input boxes overtop of our map, one for the latitude coordinate and the other for the longitude coordinate. They are bound to $scope.lat
and $scope.lon
respectively.
Creating a Helper Functions File
Once we get into the filter we’re going to need a way to check whether a value is between two numbers so that we can validate the coordinates the user has input. To do so it will be useful to have a custom function put on the Number
prototype called between
which takes two numbers and returns true if our value in question is between the two numbers and false if it isn’t.
// scripts/functions.js
// Helpful range checker provided by David Thomas: http://stackoverflow.com/a/18881828
Number.prototype.between = function (a, b, inclusive) {
var min = Math.min.apply(Math, [a,b]),
max = Math.max.apply(Math, [a,b]);
return inclusive ? this >= min && this <= max : this > min && this < max;
}
Creating the Filter
The final JavaScript file we need is for our filter which we’ll call coordinateFilter.js
. Let’s create this within the scripts directory and get the basics of it setup.
// scripts/coordinateFilter.js
(function() {
'use strict';
angular
.module('angular-coordinates')
.filter('coordinateFilter', coordinateFilter);
function coordinateFilter($sce) {
return function(coordinate, conversion, type, places) {
// The filter will be running as we type values into the input boxes, which returns undefined
// and brings up an error in the console. Here wait until the coordinate is defined
if(coordinate != undefined) {}
}
}
})();
As you can see, the filter is declared much like how a controller or directive would be—we call .filter
on our module to create a new filter, give it a name of coordinateFilter
and pass in the coordinateFilter
method that we’re creating just below. The coordinateFilter
method is going to itself return a function, and you’ll see here that we’re declaring some parameters on the function being returned. Here’s what those parameters represent:
- coordinate – the input coordinate we pass to the filter
- conversion – an argument declared when using the filter in the view which will allow us to control whether the coordinate should be converted to DMS or to DD
- type – an argument to let the filter know whether the input coordinate is a latitude or longitude value which is needed because the rules for coordinate validation differ between the two
- places – another argument which will give us the ability to change how many decimal places are returned from the conversion
We now have all of the basics setup. If everything is wired up correctly, you should see the map rendering with the input boxes overtop:
Building Out the Filter
Now that we have the structure all setup, let’s go through the logic we’ll need for the filter to work properly.
Matching User Input for Coordinates
We’ll need a way to grab only the relevant bits of user input so that we’re not trying to convert strings or any other gibberish. A good way to do this would be with a regular expression.
//...
// Check for user input that is a positive or negative number with the option
// that it is a float. Match only the numbers and not the white space or other characters
var pattern = /[-+]?[0-9]*\.?[0-9]+/g
var match = coordinate.match(pattern);
In this regular expression we are looking for only positive or negative numbers and we are excluding any whitespace or other characters. We’re also looking for floats if the user is to pass decimal degrees as their input. Afterewards we are using the JavaScript match
method to look at the coordinate the gets passed to the filter and match parts of the user input based on our regular expression. The match
method returns an array of matches.
Converting the Input to Either DMS or DD
// ...
if(conversion === "toDD" && match && coordinateIsValid(match, type)) {
// If the match array only has one item, the user has provided decimal degrees
// and we can just return what the user typed in
if(match.length === 1) {
return parseFloat(match);
}
// If the match array has a length of three then we know degrees, minutes, and seconds
// were provided so we can convert it to decimal degrees
if(match.length === 3) {
return toDecimalDegrees(match);
}
} else if(conversion === 'toDMS' && match && coordinateIsValid(match, type)) {
// When converting from decimal degrees to degrees, minutes and seconds, if
// the match array has one item we know the user has input decimal degrees
// so we can convert it to degrees, minutes and seconds
if(match.length === 1) {
return toDegreesMinutesSeconds(match);
}
// To properly format the converted coordinates we will need to add in HTML entities
// which means we'll need to bind the returned string as HTML and thus we need
// to use $sce (Strict Contextual Escaping) to say that we trust what is being bound as HTML
if(match.length === 3) {
return $sce.trustAsHtml(match[0] + '° ' + match[1] + '′ ' + match[2] + '″ ');
}
}
// Output a notice that the coordinates are invalid if they are
else if(!coordinateIsValid(match, type)) {
return "Invalid Coordinate!";
}
The block above is where we check whether the coordinate is to be convertd to DMS or to DD and then respond accordinly. In the top-level if
blocks you’ll see that we first check whether the conversion
method is toDD
or toDMS
. We also make sure that a match
exists and that the coordinate is valid. Coordinate validation is done with the coordinateIsValid
function which we will define later.
Once those checks pass, we then have two scenarios that we’ll respond to. The first is that the match
array has a length of 1 and the second is that it has a length of 3. If it has a length of 1 then we will assume that the user has entered a value in decimal degrees and we will either return the value right back to them (it’s already in DD) or we will convert it to DMS using the toDegreesMinutesSeconds
function that we’ll define later. If the length of the match
array is 3 then we will assume the user has passed a value that is in DMS and we’ll respond accordinly.
Finally, if the input doesn’t make it through the top-level if
checks, we know the coordinate is invalid and we’ll alert the user of it.
Defining the Conversion Functions
Above we called on the toDecimalDegress
and toDegreesMinutesSeconds
functions, but we haven’t defined them yet.
// ...
function toDecimalDegrees(coord) {
// Setup for all parts of the DMS coordinate and the necessary math to convert
// from DMS to DD
var degrees = parseInt(coord[0]);
var minutes = parseInt(coord[1]) / 60;
var seconds = parseInt(coord[2]) / 3600;
// When the degrees value is negative, the math is a bit different
// than when the value is positive. This checks whether the value is below zero
// and does subtraction instead of addition if it is.
if(degrees < 0) {
var calculated = degrees - minutes - seconds;
return calculated.toFixed(places || 4);
}
else {
var calculated = degrees + minutes + seconds
return calculated.toFixed(places || 4);
}
}
// This function converts from DD to DMS. Math.abs is used a lot because
// for the minutes and seconds, negative values are not valid
function toDegreesMinutesSeconds(coordinate) {
var degrees = coordinate[0].split('.')[0];
var minutes = Math.abs(Math.floor(60 * (Math.abs(coordinate[0]) - Math.abs(degrees))));
var seconds = 3600 * (Math.abs(coordinate[0]) - Math.abs(degrees) - Math.abs(minutes) / 60).toFixed(2);
return $sce.trustAsHtml(degrees + '° ' + minutes + '′ ' + seconds + '″ ');
}
The first function above allows us to convert from DMS to DD. The toDecimalDegress
function takes an array of coordinates and sets up the initial math on them. As you’ll see, degrees
is just the first value of the coordinates array, but the minutes
and seconds
variables have some math done on them to set them up for our calculations to follow.
The math we do to get the DD coordinate depends on whether the degree value is positive or negative—if it is positive, we need to add the degrees
, minutes
and seconds
together which will give us a positive float value. However, if they are negative then we need to subtract them to get the correct value. Once we have done the math on the values, we return them to a fixed number of decimal places, which is either the value that we specify in our view with an argument on the filter, or a default of 4 which is a reasonable number of places.
Since the math to convert from DD to DMS is a bit differet, the toDegreesMinutesSeconds
function is going to look a bit different as well. In this function we first define the value of degrees
by splitting the input at the decimal place and grabbing the first element in the resulting array. Since negative values are meaningless for minutes
and seconds
in DMS notation, we will require the absolute values for these to properly do the math, which we do by passing values into Math.abs
.
Finally in the toDegreesMinutesSeconds
function we are returning a string that will be directly bound to HTML in the view. To do this we need to use Angular’s Strict Contextual Escaping with $sce
. We build up the string with some HTML entities that are used for standard DMS notation.
Validating the Coordinates
There are some rules that we need to apply to the user input to make sure the coordinates are valid and they differ depending on whether the user enters a latitude or a longitude coordinate.
// ...
// This function checks whether the coordinate value the user enters is valid or not.
// If the coordinate doesn't pass one of these rules, the function will return false
// which will then alert the user that the coordinate is invalid.
function coordinateIsValid(coordinate, type) {
if(coordinate) {
// The degree values of latitude coordinates have a range between -90 and 90
if(coordinate[0] && type === 'lat') {
if(!parseInt(coordinate[0]).between(-90, 90)) return false;
}
// The degree values longitude coordinates have a range between -180 and 180
else if(coordinate[0] && type === 'lon') {
if(!parseInt(coordinate[0]).between(-180, 180)) return false;
}
// Minutes and seconds can only be between 0 and 60
if(coordinate[1]) {
if(!parseInt(coordinate[1]).between(0, 60)) return false;
}
if(coordinate[2]) {
if(!parseInt(coordinate[2]).between(0, 60)) return false;
}
}
// If the coordinate made it through all the rules above, the function
// returns true because the coordinate is good
return true;
}
The first thing we do is check whether the filter is meant to support a latitude or longitude value and then respond accordinly. We also need to check to see if a value exists at the 0th, 1st and 2nd indices on the coordinate array that is being checked, otherwise the function would return false
when it wouldn’t need to, which would mess up the output. Finally, we return true
if everything checks out which is our indication that the coordinate entered by the user is valid.
Adjusting the View to Output Conversions
Now that our filter is complete, we can add some elements to the view that will be responsible for displaying the converted coordinates.
<!-- index.html -->
<div class="info-box">
<div class="col-sm-6">
<h3>Degees, Minutes, Seconds</h3>
<h3><span class="label label-primary">Latitude</span> <span ng-bind-html="lat | coordinateFilter:'toDMS':'lat'"></span></h3>
<h3><span class="label label-primary">Longitude</span> <span ng-bind-html="lon | coordinateFilter:'toDMS':'lon'"></span></h3>
</div>
<div class="col-sm-6">
<h3>Decimal Degrees</h3>
<h3><span class="label label-primary">Latitude</span> {{ lat | coordinateFilter:'toDD':'lat':4 }}</h3>
<h3><span class="label label-primary">Longitude</span> {{ lon | coordinateFilter:'toDD':'lon':4 }}</h3>
</div>
</div>
There are two ways that we’re making use of the filter in the view. First, we’re binding values directly to HTML under the Degrees, Minutes, Seconds section. This is necessary because we wanted to return a string with the HTML entities relevant to DMS notation from the filter. We use ng-bind-html
to do this and specify that we want to output lat
and long
. We use the pipe character to denote that we’re using a filter and the call the coordinateFilter
. You can see that we’re also passing the arguments we talked about earlier which are conversion
and type
.
Things work similarly in the Decimal Degrees section, but there we are calling the filter directly in the templating. In that section we’re also specifying a third argument for the filter which is the number of decimal places we want the output to go to, which in our case is four places.
Once the .info-box
is ready in our view, you should see the converted coordinates at the bottom of the page:
Using Filters in the Controller
Angular also allows us to use filters in our controllers which can come in handy in some cases. This is true in our case because we’ll need to give Angular Google Maps coordinates in DD notation for it to properly update the center and marker position as coordinates are entered by the user.
To make use of filters in our controllers we can simply inject them. All filters in Angular have Filter
appended to the filter name, so we’ll need to inject coordinateFilterFilter
.
// scripts/mapController.js
(function() {
'use strict';
angular
.module('angular-coordinates')
.controller('mapController', map);
function map($scope, coordinateFilterFilter) {
$scope.map = {
center: {
latitude: $scope.lat || 62.4568,
longitude: $scope.lon || -114.3964
},
zoom: 7
}
$scope.marker = {
id: 0,
coords: {
latitude: $scope.lat || 62.4568,
longitude: $scope.lon || -114.3964
},
options: {
draggable: false
}
}
var updateCenter = function() {
$scope.marker.coords = {
latitude: parseFloat(coordinateFilterFilter($scope.lat, 'toDD')) || 62.4568,
longitude: parseFloat(coordinateFilterFilter($scope.lon, 'toDD')) || -114.3964
}
$scope.map.center = {
latitude: parseFloat(coordinateFilterFilter($scope.lat, 'toDD')) || 62.4568,
longitude: parseFloat(coordinateFilterFilter($scope.lon, 'toDD')) || -114.3964
}
}
$scope.$watch('lat', updateCenter);
$scope.$watch('lon', updateCenter);
}
})();
As you can see, we’ve injected coordinateFilterFilter
and we are using it in the new updateCenter
function. This function is used along with Angular’s $watch
to look for updates to values. If a value change is detected, the updateCenter
function is triggered. Since the user will in many cases be entering coordinates in DMS notation, and since that format isn’t accepted by Angular Google Maps, we’ll need to convert to DD notation first.
Since we’re not using the pipe character to make use of the filter like we do in the view, we need to pass arguments like we would to any normal function. When we call coordinateFilterFilter
here, the first argument we pass in is the input from the user and the second is what kind of conversion we are doing. We then use JavaScript’s ||
operator to default to our home values so that our marker doesn’t end up in the middle of nowhere.
Wrapping Up
In this tutorial we’ve seen how we can create a complex AngularJS custom filter and use it a number of different ways. We saw how we can bind values that are returned from the filter directly to HTML using Angular’s $sce
and also how we can make use of filters in our controllers.
I’d love to hear about any suggestions for improvements that you might have for this filter. Feel free to leave a comment below or get in touch by email. The code for this tutorial can be found on GitHub and there is a demo site here.