Rapid chrome app development with angular

The Chrome web browser is Google’s custom browser. Not only is it incredibly speedy and on the bleeding edge of web development, it is at the forefront of delivering web experiences both on and off the web.

Chrome Apps are embedded applications that run within the web browser, but are intended on delivering a native app feel. Since they run within Chrome itself, they are written in HTML5, javascript, CSS3, and have access to native-like capabilities that true web applications do not.

Chrome apps have access to the Chrome API and services and can provide a integrated desktop-like experiences to the user.

One more interesting differentiation between Chrome apps and webapps is that they always load locally, so they show up immediately, rather than waiting for the network to fully download the components. This greatly improves the performance and our user’s experience with running our apps.

In this article, we’ll walk through how to create an advanced Chrome application using Angular. We’re going to create a clone of the fantastic chrome webapp Currently by the team at Rainfall.

Currently

We’ll be building a clone that we’ll call Presently:

Presently

Understanding the Chrome apps

Let’s dive into looking at how Chrome apps actually work and how we can start building our own.

Every Chrome application has three core files:

manifest.json

The manifest.json file that describes the meta-data about the application, such as the name, description, version, and how to launch our application.

A background script

The background script that sets up how our application responds to system-level events, such as a user installing our app or launching it, etc.

A view

Most Chrome applications have a view. This component is optional, but will most generally always be used for our applications.

Architecting Presently

When we’re building Presently, we’ll need to take into account the application architecture. This will give us insight into how we’ll build the app when we get to code.

Like Currently, Presently will be a “newtab” app. This means that it will launch every time we open a new tab.

Presently has two main screens:

The home screen

This is the screen that features the current time and the current weather. It also features several weather icons beside the weather.

The settings screen

This screen will allow our users to change their location within the app.

In order to support the home screen, we’ll need to be able to show a properly formatted date and time as well as fetch weather from a remote API service.

To support the settings screen, we’ll integrate with a remote API service to auto-suggest potential locations for an input box.

Finally, we’ll use the basic localstorage (session storage) to persist our settings across the app.

Building the skeleton

Building our app, we’ll set up a file structure like so:

File structure

We’ll place our css files in css/, our custom fonts in fonts/, and our javascript files in js/. The main javascript file will be set in the js/app.js file and the HTML for our app will be placed in tab.html at the root.

There are great tools to help bootstrap Chrome app extensions such as yeoman.

Before we can start up our Chrome extension, we’ll need to grab a few dependencies.

We’ll grab the latest version of angular.min.js (snapshot version) as well as angular-route.min.js from angularjs.org and save them to the js/vendor/ directory.

Note that these versions are the snapshot versions of angular, not the release candidates.

Lastly, we’ll use twitter’s bootstrap 3 framework to style our app, so we’ll need to get the bootstrap.min.css and save it to css/ from getbootstrap.com.

In production, it’s often more efficient when working with multiple developers to use a tool like Bower to manage dependencies. Since we’re building a newtab app, however it’s important we keep our app lightweight so it launches quickly.

manifest.json

With every Chrome app we’ll write, we’ll need to set up a manifest.json. This manifest tells Chrome how the application should run, what files it should use, what permissions it has, etc. etc.

Our manifest.json will need to describe our app as newtab app as well as describing the content_security_policy (the policies that describe what our application can and cannot do) and the background script (needed by Chrome).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{ "manifest_version": 2, "name": "Presently", "description": "A currently clone", "version": "0.1", "permissions": [ "http://api.wunderground.com/api/", "http://autocomplete.wunderground.com/api" ], "background": { "scripts": ["js/vendor/angular.min.js"] }, "content_security_policy": "script-src 'self'; object-src 'self'", "chrome_url_overrides" : { "newtab": "tab.html" } }

The manifest.json is relatively straightforward with the name, the manifest version, the version, etc. In order to tell Chrome to launch our app as a newtab app, we set the app to override the newtab page.

tab.html

The main HTML file for our application is the tab.html file. This is the file that will be loaded when we open a new tab in Chrome.

We’ll set up the basic angular app inside of the tab.html file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html> <html data-ng-app="myApp" data-ng-csp=""> <head> <meta charset="UTF-8"> <title>Presently</title> <link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/main.css"> </head> <body> <div class="container"> </div> <script src="./js/vendor/angular.min.js"></script> <script src="./js/vendor/angular-route.min.js"></script> <script src="./js/app.js"></script> </body> </html>

This very basic structure of an angular application looks almost identical to any angular app, with one exception: data-ng-csp="".

The ngCsp directive enables Content Security Policy (or CSP) support for our angular app. Since Chrome apps prevent the browser from using eval or function(string) generated functions and Angular uses the function(string) generated function for speed, ngCsp will cause Angular to evaluate all expressions.

This compatibility mode comes as a cost of performance, however as it will execute operations much slower, but will not throw any security violations in the process.

CSP also forbids javascript files from inlining stylesheet rules, so we’ll need to include angular-csp.css manually.

The angular-csp.css file can be found at http://code.angularjs.org/snapshot/angular-csp.css.

Lastly, ngCsp must be placed alongside the root of our angular apps:

1
<html ng-app ng-csp>

Without the ng-csp directive, our Chrome app will not run as it will throw a security exception. If you see a security exception being thrown, make sure you check the root element for the directive.

Loading the app in Chrome

With our app in progress, let’s load it into Chrome so we can follow our progress along in the browser. To load our app in Chrome, navigate to the url: chrome://extensions/.

Once there, click on the button “Load unpackged extension…” and find the root directory (the directory that contains our manifest.json file from above).

Load unpacked extension

Once the application has been loaded into the Chrome browser, open a new tab and we should see our empty app with one error (don’t worry, we’ll fix this shortly):

First run

Anytime that we update or modify our manifest.json file, we’ll need to click on the Reload link underneath our Chrome app in chrome://extensions.

The main module

Our entire angular application will be built in the js/app.js file. For production versions of our app, we may want to split this functionality into multiple files or use a tool like grunt to compress and concatenate them for us.

Our app is called myApp, so we’ll create an angular module with the same name:

1
angular.module('myApp', [])

With this, our app will run in the browser without any issues.

Building the homepage

We’ll start by building the home section in our app. In this section, we’ll work on putting together components of our app that will make the application run. In the next section, we’ll set up the multi-route application.

Building the clock

The main feature of Presently is the large clock that sits right at the top of the application and updates every second. In Angular, we can set this up pretty simply.

We’ll first start by building a MainCtrl will be responsible for managing the home screen. Inside this MainCtrl controller, we’ll set up a timeout that will tick every second and update a local scope variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
angular.module('myApp', []) .controller('MainCtrl', function($scope, $timeout) { // Build the date object $scope.date = {}; // Update function var updateTime = function() { $scope.date.raw = new Date(); $timeout(updateTime, 1000); } // Kick off the update function updateTime(); });

Every second that our MainCtrl is visible, the updateTime() function will be ran to update the $scope.date.raw timestamp and our view will be updated.

In order for us to see anything in the view load in our Chrome app, we’ll need to bind this data to the document. We can set up this binding using the normal {{ }} template syntax:

1
2
3
4
5
<div class="container"> <div ng-controller="MainCtrl"> {{ date.raw }} </div> </div>

When we go back to the browser and refresh, we’ll see an unformatted Date object ticking in the view:

Unformatted date

The date is very ugly in the browser as it stands now. We can utilize Angular’s built-in filters to format our date in a much more elegant manner.

Following along with how Currently formats the date in their homescreen, we’ll format ours similarly. Updating the view, we will move our date into it’s own nested div and add formatting to display the date:

1
2
3
4
5
6
7
<div class="container"> <div ng-controller="MainCtrl"> <div id="datetime"> <h1>{{ date.raw | date:'hh mm ss' }}</h1> </div> </div> </div>

With a little CSS and help from bootstrap, our dates will appear on the screen in a much more human-friendly format.

First screen

We’re using the CSS rules to align the date and times to the center of the screen and increasing the font-size to be prominently displayed on-screen.

1
2
3
4
5
6
#datetime { text-align: center; } #datetime h1 { font-size: 6.1em; }

We can add a second date in our view that simply shows our date with a human-friendly display. This is simply a matter of adding a second formatted date:

1
2
3
4
5
6
<!-- ... --> <div id="datetime"> <h1>{{ date.raw | date:'hh mm ss' }}</h1> <h2>{{ date.raw | date:'EEEE, MMMM yyyy' }}</h2> </div> <!-- ... -->

Our CSS for the #datetime h2 tag simply increases the size of the <h2> tag:

1
2
3
#datetime h2 { font-size: 1.0em; }

Full dates

Sign up for wunderground’s weather API

Our app will need to reach out to foreign sources to fetch the current weather for the location we’re interested in. In this application, we’re using the wunderground api.

In order to use the wunderground api, we’ll need to get an access api key.

To get an access api key, we’ll need to sign up first. Head to the weather api wunderground page at http://www.wunderground.com/weather/api/ and click “Sign Up for Free!”.

Sign up for wunderground

Fill out the relevant details on the following page and we’ll click through until we reach the detail page that shows our API key.

Fill out details

Once we’re set, locate the wunderground api key and save it. We’ll be using it shortly.

Building the angular service

We won’t place our logic into the Controller to fetch the weather as it is both inefficient (as the controller will be blown away when we navigate to another page and we’ll need to re-call the api every time the controller is loaded) and poor design to mix in business logic details with implementation details.

Instead, we’ll use a service. A service persists across controllers, for the duration of the application’s lifetime and is the appropriate place for us to hide business logic away from the controller.

As we’ll need to configure our app when it boots up, we’ll use the .provider() method of creating a service. This is the only method for creating services that can be injected into .config() functions.

To build the service, we’ll use the .provider() api method that takes both a name of the service as well as a function that defines the actual provider.

1
2
3
angular.module('myApp', []) .provider('Weather', function() { })

Inside here, we’ll need to define a $get() function that returns the methods available to the service. To configure this service, we’ll need to allow a method for the api key to be set on configuration. These methods will live outside of the scope of the $get() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
.provider('Weather', function() { var apiKey = ""; this.setApiKey = function(key) { if (key) this.apiKey = key; }; this.$get = function($http) { return { // Service object } } })

With this minimal amount of code, we can now inject the Weather service into our .config() function and configure the service with our wunderground api key.

When angular encounters a provider created with the .provider() api method, it creates a [Name]Provider injectable object. This is what we’ll inject into our config function:

1
2
3
4
5
6
7
8
9
// .provider('Weather', function() { // ... // }) .config(function(WeatherProvider) { WeatherProvider.setApiKey('YOUR_API_KEY'); }) // .controller('MainCtrl', function($scope, $timeout) { // ...

The wunderground API requires that we pass the API key with our request in the URL. In order to pass our api key in with every request, we’ll create a function that will generate the url.

1
2
3
4
5
6
7
var apiKey = ""; // ... this.getUrl = function(type, ext) { return "http://api.wunderground.com/api/" + this.apiKey + "/" + type + "/q/" + ext + '.json'; };

Now, we can create our API call for the Weather service to get us the latest forecast data from the wunderground API.

We’ll create our own promises that we can use to resolve in the view as we’ll want to return only the relevant results from our API call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
this.$get = function($q, $http) { var self = this; return { getWeatherForecast: function(city) { var d = $q.defer(); $http({ method: 'GET', url: self.getUrl("forecast", city), cache: true }).success(function(data) { // The wunderground API returns the // object that nests the forecasts inside // the forecast.simpleforecast key d.resolve(data.forecast.simpleforecast); }).error(function(err) { d.reject(err); }); return d.promise; } } }

Now, we can inject the Weather service into our controller and simply call the method getWeatherForecast() and respond to the promise instead of dealing with the complexity of the API in our controller.

Back to our MainCtrl, we can inject the Weather service and set the result on our scope:

1
2
3
4
5
6
7
8
9
10
11
.controller('MainCtrl', function($scope, $timeout, Weather) { // ... $scope.weather = {} // Hardcode San_Francisco for now Weather.getWeatherForecast("CA/San_Francisco") .then(function(data) { $scope.weather.forecast = data; }); // ...

To view the result of the API call in our view, we’ll need to update our tab.html. For debugging purposes, we like to use the json filter inside a <pre> tag:

1
2
3
<div id="forecast"> <pre>{{ weather.forecast | json }}</pre> </div>

Weather API debugging call

We can see that the view is updated with the latest weather and now we’re good to go to create a more polished view.

The view itself will iterate over the forecast.forecastday collection. For each element, we’ll create a view that displays the weather icon given to us by the wunderground api as well as the human-readable date and high.

1
2
3
4
5
6
7
8
9
10
11
12
<div id="forecast"> <ul class="row list-unstyled"> <li ng-repeat="day in weather.forecast.forecastday" class="col-md-3"> <div ng-class="{today: $index == 0}"> <img class="{{ day.icon }}" ng-src="{{ day.icon_url }}" /> <h3>{{ day.high.fahrenheit }}</h3> <h4 ng-if="$index == 0">Now</h4> <h4 ng-if="$index != 0">{{ day.date.weekday }}</h4> </div> </li> </ul> </div>

Clean HTML weather

The style we’ve set in the view is set as:

1
2
3
4
5
6
7
8
9
10
#forecast ul li { font-size: 4.5em; text-align: center; } #forecast ul li h3 { font-size: 1.4em; } #forecast ul li .today h3 { font-size: 1.8em; }

A settings screen

Currently our app only has one view, with a hard-coded city that fetched for every browser. Although this works for all of us here in San Francisco, it does not work for anyone outside of it.

In order to allow our users the ability to customize their experience with Presently, we’ll need to add a second screen: a setting screen.

To introduce a second screen (and multiple views), we’ll need to add the ngRoute module as a dependency of our app module.

1
angular.module('myApp', ['ngRoute'])

Now we can define our separate views and routes as well as pull our home screen view out of the main tab.html view.

In defining our routes, note that we’ll need two; one for each of the two different screens of our app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// angular.module(...) // ... .config(function($routeProvider) { $routeProvider .when('/', { templateUrl: 'templates/home.html', controller: 'MainCtrl' }) .when('/settings', { templateUrl: 'templates/settings.html', controller: 'SettingsCtrl' }) .otherwise({redirectTo: '/'}); })

Now we can take our entire tab.html html between the .container div, move it into the file templates/home.html, and replace it with <div ng-view></div>.

1
2
3
<div class="container"> <div ng-view></div> </div>

When we refresh the page, we’ll see that nothing has appeared to have changed, but our html is no longer loaded inside the tab.html, but from the templates/home.html template.

Currently, we have no way of navigating between our two screens. We can add some footer-based navigation that we can allow our users to navigate between the pages. We’ll simply add two links at the bottom of the page to navigate between pages, like so:

1
2
3
4
5
6
<div id="actionbar"> <ul class="list-inline"> <li><a class="glyphicon glyphicon-home" href="#/"></a></li> <li><a class="glyphicon glyphicon-cog" href="#/settings"></a></li> </ul> </div>

In order to add them to the the bottom right-hand corner of the screen, we’ll apply a bit of CSS to absolutely position them:

1
2
3
4
5
6
7
8
9
#actionbar { position: absolute; bottom: 0.5em; right: 1.0em; } #actionbar a { font-size: 2.2rem; color: #000; }

Now, if we navigate to our settings page by clicking on the cog button, we’ll see that nothing is rendered. We need to define our SettingsCtrl so we can start manipulating the view and working with our user.

1
2
3
4
5
// ... .controller('SettingsCtrl', function($scope) { // Our controller will go here })

The settings screen itself will feature a single form that will be responsible for allowing the user to change cities that they are interested in. The HTML itself will look similar to this (with a few features we have yet to implement):

1
2
3
4
5
6
7
8
<h2>Settings</h2> <form ng-submit="save()"> <input type="text" ng-model="user.location" placeholder="Enter a location" /> <input class="btn btn-primary" type="submit" value="Save" /> </form>

Implementing a User service

For the same reasons that we are hiding away the complexity of the wunderground API, we’ll also hide away our User api. This will enable us to use localstorage as well as communicate across our controllers about the user settings at any part of the app.

The UserService itself is straightforward and does not need to be configured in our app. Without the use of localstorage, our UserService will simply be:

1
2
3
4
5
6
7
8
9
10
11
// ... .factory('UserService', function() { var defaults = { location: 'autoip' }; var service = { user: defaults }; return service; })

This service will hold on to our user object for the lifetime of the application. That is to say, while the browser window is open, the settings of the application will remain constant to the user’s settings. However, if our user opens a new tab in chrome, these settings disappear, which is not ideal.

We can persist our settings across our app by using Chrome’s sessionStorage capabilities. Luckily, this api is straightforward and simple.

We’ll add two functions to the UserService:

Even with these capabilities, the UserService has not grown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ... .factory('UserService', function() { var defaults = { location: 'autoip' }; var service = { user: {}, save: function() { sessionStorage.presently = angular.toJson(service.user); }, restore: function() { // Pull from sessionStorage service.user = angular.fromJson(sessionStorage.presently) || defaults return service.user; } }; // Immediately call restore from the session storage // so we have our user data available immediately service.restore(); return service; }) // ...

Now, we can inject this UserService across our Chrome app and have access to the same user data. Heading back to our SettingsCtrl, we can now set up a user object to define settings with the new service:

1
2
3
4
.controller('SettingsCtrl', function($scope, UserService) { $scope.user = UserService.user; });

If we refresh the browser, we’ll now see that we have a default set for the user as 'autoip', which is the default we set up in the UserService definition.

Settings page

Now, we only need a way for our user to save their data into their session storage so we can use it across the app. In our templates/settings.html, we defined the form as having a ng-submit="save()" action, thus when our user submits the form, the save() function will be called.

Inside our SettingsCtrl, we’ll implement the save() function that will call save on the UserService and persist the user’s data into their sessionStorage.

1
2
3
4
5
6
7
8
.controller('SettingsCtrl', function($scope, UserService) { $scope.user = UserService.user; $scope.save = function() { UserService.save(); } });

Now, with the single input field bound to user.location, if we change the value and press save, our user’s sessionStorage will be updated:

Session storage

By using the UserService in our HomeCtrl, we can now remove the hardcoded value of ‘“CA/San_Francisco”’ and replace it with our new UserService object’s location.

1
2
3
4
5
6
7
8
9
10
11
// ... .controller('MainCtrl', function($scope, $timeout, Weather, UserService) { // ... $scope.user = UserService.user; Weather.getWeatherForecast($scope.user.location) .then(function(data) { $scope.weather.forecast = data; }); // ... })

As we can see, if we flip back and forth from the settings view and input “NY/New_York”, for instance we can see the weather changing based upon the location we place in the settings page.

New York location

City autofill/autocomplete

It’s pretty inconvenient to need to type a city that conforms to the wunderground API formats (lat/long, city and state, country codes, etc). Luckily, the wunderground api also provides us an autocomplete API.

Instead of requiring our users to know the specific city format, we’ll provide a list of them for our user’s to select.

For simplicity and flexibility purposes, we’re only going to create a raw javascript-based autocomplete, rather than use a plugin library, such as the typeahead.js or jQuery plugin libraries.

To do this, we’ll create a directive that we’ll place on the <input> element that will append a <ul> element with a list of suggested places.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.directive('autoFill', function($timeout) { return { restrict: 'EA', scope: { autoFill: '&', ngModel: '=' }, compile: function(tEle, tAttrs) { // Our compile function return function(scope, ele, attrs, ctrl) { // Our link function } } } })

As we will be creating a new element, we’ll need to use the compile function, rather than just the link function and a template, since our <ul> element cannot be nested underneath an <input> element.

Without diving too deeply into how the compile function works, we’re going to create a new element and we’ll set up the bindings on our new element:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ... compile: function(tEle, tAttrs) { var tplEl = angular.element('<div class="typeahead">' + '<input type="text" autocomplete="off" />' + '<ul id="autolist" ng-show="reslist">' + '<li ng-repeat="res in reslist" ' + '>{{res.name}}</li>' + '</ul>' + '</div>'); var input = tplEl.find('input'); input.attr('type', tAttrs.type); input.attr('ng-model', tAttrs.ngModel); tEle.replaceWith(tplEl); return function(scope, ele, attrs, ctrl) { // ...

Inside of our link function, we’ll bind on a keyup event and check that we have at least a minimum number of characters in our input field. Once there are a minimum number of characters, we’ll run a function set by the use of the directive to fetch the auto-suggested values.

auto-complete api

Examining how we invoke this directive, we call it by passing a function to the auto-fill directive call as well as binding the location to the user.location value:

1
2
3
4
5
<input type="text" ng-model="user.location" auto-fill="fetchCities" autocomplete="off" placeholder="Location" />

In our Weather service, we’ll create another function that specifically calls the autocomplete api and resolves a promise with a list of suggestions completions for a query term.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getWeatherForecast: function(city) { // ... }, getCityDetails: function(query) { var d = $q.defer(); $http({ method: 'GET', url: "http://autocomplete.wunderground.com/' + 'aq?query=" + query }).success(function(data) { d.resolve(data.RESULTS); }).error(function(err) { d.reject(err); }); return d.promise; }

Back in our SettingsCtrl, we can simply reference this function as the function that retrieves the list of suggested values. Remember, we’ll need to inject the Weather service in the controller to reference it.

1
2
3
4
5
.controller('SettingsCtrl', function($scope, UserService, Weather) { // ... $scope.fetchCities = Weather.getCityDetails; });

In the directive, we can now call this function that we’ll reference when we create.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ... tEle.replaceWith(tplEl); return function(scope, ele, attrs, ctrl) { var minKeyCount = attrs.minKeyCount || 3, timer, input = ele.find('input'); input.bind('keyup', function(e) { val = ele.val(); if (val.length < minKeyCount) { if (timer) $timeout.cancel(timer); scope.reslist = null; return; } else { if (timer) $timeout.cancel(timer); timer = $timeout(function() { scope.autoFill()(val) .then(function(data) { if (data && data.length > 0) { scope.reslist = data; scope.ngModel = data[0].zmw; } }); }, 300); } }); // Hide the reslist on blur input.bind('blur', function(e) { scope.reslist = null; scope.$digest(); }); }

We’re using a timeout so that we only call the function once we are done typing. This is a simple way to prevent the function from being called repeatedly while we’re really only interested in the first call to the suggestion API.

Autofill in the browser

Sprinkling in timezone support

Finally, we also want our clock to update and reflect the new location that the user has set in their settings. Updating the clock to include timezone support is easy to add as we’ve implemented the most difficult part already through the autocomplete API.

First, we’ll add one more attribute to our directive usage as timezone:

1
2
3
4
5
6
<input type="text" ng-model="user.location" timezone="user.timezone" auto-fill="fetchCities" autocomplete="off" placeholder="Location" />

Next, we’ll need to add the timezone attribute on to our generated <input> field in our directive’s compile function:

1
2
3
4
5
6
7
// ... input.attr('type', tAttrs.type); input.attr('ng-model', tAttrs.ngModel); input.attr('timezone', tAttrs.timezone); tEle.replaceWith(tplEl); // ...

Last, but not least, we’ll simply save the user’s timezone when we save the topmost value for the user’s location in the autocomplete link function:

1
2
3
4
5
6
// ... scope.reslist = data; scope.ngModel = data[0].zmw; scope.timezone = data[0].tz; // ...

Back in the browser, when we type our city we’ll also save the timezone along with the new value of the city as well:

Timezone support

Finally, we’ll need to update the time and date in our MainCtrl to take into account the new timezone.

Previously, matching timezone names to their GMT offsets was a difficult task. The Mozilla and Chrome teams have implemented the toLocaleString with the new timeZone argument that enables us to remap a date according to it’s timezone. Since we are writing a Chrome app, we can depend upon this function being available for us to use in our app.

Back in our MainCtrl, we’ll create a new Date based off the saved timezone:

1
2
3
4
5
6
7
8
9
10
11
12
.controller('MainCtrl', function($scope, $timeout, Weather, UserService) { $scope.date = {}; var updateTime = function() { $scope.date.tz = new Date(new Date().toLocaleString( "en-US", {timeZone: $scope.user.timezone} )); $timeout(updateTime, 1000); } // ...

Now, instead of using the $scope.date.raw in our view, we’ll switch over to using the $scope.date.tz. Now, the time will change along with the timezone.

In chicago: Chicago

In Hawaii: Hawaii

Our upcoming book, ng-book: The Complete Book on AngularJS features some more techniques for how to allow our users to customize the UI along with a lot of other professional AngularJS information.

The complete source code for this article is available here. Enjoy!

Get the weekly email all focused on AngularJS. Sign up below to receive the weekly email and exclusive content.
We will never send you spam and it's a cinch to unsubscribe.

Download a free sample of the ng-book: The Complete Book on AngularJS

ng-book: The Complete Book on AngularJS is the canonical AngularJS book available today.

It's free, so just enter your email address and the PDF will be sent directly to your inbox. Mailchimp can take up to an hour to deliver the free sample chapter, but if you don't receive it within the hour, send us an email and we'll manually send them to you!

We'll send you updates about the book, when it updates and other free content.

We will never send you spam and it's a cinch to unsubscribe.

Comments

comments powered by Disqus