Accessing Live Weather Data

There are a variety of weather services you can use, but for this app, we will use the Dark Sky service. Sign up for a developer account on theDark Sky register page.

Once you have signed up, you will be issued an API key. We will need this to use the API. Currently, the API allows up to 1,000 calls per day before you have to pay for usage.

The call to the API is actual quite simple:

https://api.darksky.net/forecast/APIKEY/LATITUDE,LONGITUDE

If we just replaced our local data call in the_weather-data.ts_file with this URL (replacing the APIKEY with your API key, and also hardcoding a location), you would find that it will not work. Opening the JavaScript console, you would see an error like this:

XMLHttpRequest 
cannot load https://api.darksky.net/forecast/APIKEY/LATITUDE,LONGITUDE. 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://localhost:8100' is therefore not allowed access.

This error happens due to the browser’s security policies, which are essentially a way to block access to data from other domains. It is also referred to as CORS (Cross-Origin Resource Sharing). Typically, solving this issue during development requires setting up a proxy server or some other workaround. Thankfully, the Ionic CLI has a built-in workaround.

Inionic.config.json(which can be found at the root level of our app), we can define a set of proxies forionic serveto use. Just add in aproxiesproperty and define the path and the proxyUrl. In our case it, will look like this:

{
  "name": "Ionic2Weather",
  "app_id": "",
  "type": "ionic-angular",
  "proxies": [
    {
      "path": "/api/forecast",
      "proxyUrl": "https://api.darksky.net/forecast/APIKEY"
    }
  ]
}

Replacing the APIKEY in the proxyURL with your actual API key.

Save this file, making sure to restart$ ionic servefor these changes to take effect.

Returning back to the_weather-service.ts_file, we can update the http.get call to be:

this.http.get('/api/forecast/43.0742365,-89.381011899')...

Our application will be able to now properly call the Dark Sky API, and our live weather data will be returned to our app.

Connecting the Geolocation and Weather Providers

Currently our latitude and longitude values are hardcoded into the Dark Sky request. Let’s address this issue.

Obviously, we need to know our current position. We have that code already in place with theGeolocationfunction. Since we are using the Ionic Native wrapper to the Cordova plug-in, this is already sent up as a Promise. One of the advantages of using Promises is the ability to chain them together, which is exactly what we need to do here. Once we have our location, then we can call Dark Sky and get our weather data.

To chain Promises together, you just continue on using the.thenfunction:

geolocation.getCurrentPosition().then(pos =
>
 {
  console.log('lat: ' + pos.coords.latitude + ', lon: ' + pos.coords.longitude);
  this.currentLoc.lat = pos.coords.latitude;
  this.currentLoc.lon = pos.coords.longitude;
  this.currentLoc.timestamp = pos.timestamp;
  return this.currentLoc;
 }).then(currentLoc =
>
 {
   weatherService.getWeather(currentLoc).then(theResult =
>
 {
   this.theWeather = theResult;
   this.currentData = this.theWeather.currently;
   this.day1 = this.theWeather.daily.data[0];
   this.day2 = this.theWeather.daily.data[1];
   this.day3 = this.theWeather.daily.data[2];
   loader.dismiss();
  });
 });

Besides adding.then, we have to add the returnthis.currentLocwithin the Geolocation Promise. By adding a return, it enables us to pass this data along the Promise chain.

DEMO CODE WARNING

We are being a bit sloppy here and not accounting for any errors. With any network calls to a remote system, you should always expect failure and code for that case.

This is a great example of how using Promises can make work with asynchronous processes so much easier.

Another minor tweak to the app is to remove the duration value in our loading dialog:

let loader = this.loadingCtrl.create({
  content: "Loading weather data..."
});
loader.present();

We will let the loading dialog now stay up until we finally get our weather data. Once we do, we can simply callloading.dismiss()and remove the loading dialog.

Now we need to update our_weather-service.ts_to support dynamic locations.

First, import our custom location class:

import { CurrentLoc } from '../../interfaces/current-loc';

Next, change theloadfunction to accept our current location:

load(currentLoc:CurrentLoc) {

Then modify thehttp.getcall to reference this information:

this.http.get('/api/forecast/'+currentLoc.lat+','+currentLoc.lon)

Finally, we also need to adjust thegetWeathermethod as well to support using a location:

getWeather(currentLoc:CurrentLoc) {
  this.data = null;
  return this.load(currentLoc).then(data =
>
 {
    return data;
  });
}

We also clear the weather data that was previously retrieved by the service before we load new data. Here is the revised_weather-service.ts_file:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { CurrentLoc } from '../interfaces/current-loc'

@Injectable()
export class WeatherService {
  data: any = null;

  constructor(public http: Http) {
    console.log('Hello WeatherService Provider');
  }

  load(currentLoc:CurrentLoc) {
    if (this.data) {
      return Promise.resolve(this.data);
    }

    return new Promise(resolve =
>
 {
      this.http.get('/api/forecast/'+currentLoc.lat+','+currentLoc.lon)
        .map(res =
>
 res.json())
        .subscribe(data =
>
 {
          this.data = data;
          resolve(this.data);
        });
    });
  }

  getWeather(currentLoc:CurrentLoc) {
    this.data = null;
    return this.load(currentLoc).then(data =
>
 {
      return data;
    });
  }
}

Save the files, reload the application, and live local weather information will be shown on the page.

Getting Other Locations’ Weather

It is nice to know the weather where you are, but you might also want to know the weather in other parts of the world. We will use the side menu to switch to different cities, as inFigure 9-4.

Figure 9-4.Ionic weather app’s side menu

The first set of changes we are going to make is to thepagesarray in the_app.component.ts_file. First, let’s import ourCurrentLocclass:

import { CurrentLoc } from '../interfaces/current-loc';

Next, we need to include the location’s latitude and longitude data that we can reference to look up the weather. Since neither the Edit Locations nor the Current Locations will have predefined locations, this new property will be declared as optional:

pages: Array
<
{title: string, component: any, icon: string, loc?:CurrentLoc}
>
;

Next, we will fill this array with some locations:

this.pages = [
  { title: 'Edit Locations', component: LocationsPage, icon: 'create' },
  { title: 'Current Location', component: WeatherPage, icon: 'pin' }, 
  { title: 'Cape Canaveral, FL', component: WeatherPage, icon: 'pin', ↵
    loc: {lat:28.3922, lon:-80.6077} },
  { title: 'San Francisco, CA', component: WeatherPage, icon: 'pin', ↵
    loc: {lat:37.7749, lon:-122.4194} },
  { title: 'Vancouver, BC', component: WeatherPage, icon: 'pin', ↵
    loc: {lat:49.2827, lon:-123.1207} },
  { title: 'Madison, WI', component: WeatherPage, icon: 'pin', ↵
    loc: {lat:43.0742365, lon:-89.381011899} }
];

Now, when we select one of the four cities, we now know its latitude and longitude. But we need to pass this data along to our weather page. In Ionic 1, we would have used$stateParamsto do this; but in Ionic 2, we can just pass an object as a parameter to theNavController. So, ouropenPagefunction will now pass this data (assuming it is there):

openPage(page) {
  // Reset the content nav to have just this page
  // we wouldn't want the back button to show in this scenario
  if (page.hasOwnProperty('loc') ) {
    this.nav.setRoot(page.component, {geoloc: page.loc});
  } else {
    this.nav.setRoot(page.component);
  }
}

Go ahead and save this file, and openweather.ts. Retrieving the data is actually just a simpleget()call. We will assign the data to a variable namedloc. Add this code after we display the loading dialog:

let loc = this.navParams.get('geoloc');

If this variable is undefined, we will make the call to theGeolocationmethod like before. But, if this value is defined, then we will use that data to call the weather service with it:

if (loc === undefined) {
  geolocation.getCurrentPosition().then(pos =
>
 {
    console.log('lat:' + pos.coords.latitude + ', lon:' + pos.coords.longitude);
    this.currentLoc.lat = pos.coords.latitude;
    this.currentLoc.lon = pos.coords.longitude;
    this.currentLoc.timestamp = pos.timestamp;
    return this.currentLoc;
  }).then(currentLoc =
>
 {
    weatherService.getWeather(currentLoc).then(theResult =
>
 {
      this.theWeather = theResult;
      this.currentData = this.theWeather.currently;
      this.day1 = this.theWeather.daily.data[0];
      this.day2 = this.theWeather.daily.data[1];
      this.day3 = this.theWeather.daily.data[2];
      loader.dismiss();
    });
   });
 } else {
   this.currentLoc = loc;
   weatherService.getWeather(this.currentLoc).then(theResult =
>
 {
     this.theWeather = theResult;
     this.currentData = this.theWeather.currently;
     this.day1 = this.theWeather.daily.data[0];
     this.day2 = this.theWeather.daily.data[1];
     this.day3 = this.theWeather.daily.data[2];
     loader.dismiss();
   });
}

If we have a location, we will first assign it to the currentLoc variable, then call ourgetWeatherfunction.

Save this file and test it out. Hopefully, the weather is different in each of the locations.

However, our page title is not updating after we switch locations. This is actually another simple fix. First, we need to pass the location’s name along with its location. Inapp.component.ts, change:

this.nav.setRoot(page.component, {geoloc: page.loc});

to:

this.nav.setRoot(page.component, {geoloc: page.loc, title: page.title});

Inweather.ts, we need to make two changes. First, we need a variable to store our page title in so the template can reference it. In the class constructor, we will add this:

pageTitle: string = 'Current Location';

Then, within our code block that gets the weather for the other locations, we can get the other NavParam and assign it to thepageTitle:

this.pageTitle = this.navParams.get('title');

The last minor change is to the template itself. We need to update the<ion-title>tag to reference thepageTitlevariable:

<
ion-title
>
{{pageTitle}}
<
/ion-title
>

And with that, our page title should now display our current location’s name.

Pull to Refresh: Part 2

You might have been wondering why we bothered to set the currentLoc to the location that was passed in. We can add it directly into ourgetWeatherfunction. Remember that pull to refresh component we added? Yeah, that one. We actually never had it do anything except close after two seconds. Let’s update this code to actually get new weather. All we have to do is replace thesetTimeout, so thedoRefreshfunction becomes:

doRefresh(refresher) {
  this.weatherService.getWeather(this.currentLoc).then(theResult =
>
 {
    this.theWeather = theResult;
    this.currentData = this.theWeather.currently;
    this.day1 = this.theWeather.daily.data[0];
    this.day2 = this.theWeather.daily.data[1];
    this.day3 = this.theWeather.daily.data[2];
    refresher.complete();
  });
}

API LIMITS

Within thedoRefreshwould be a great place to check that timestamp property to prevent API abuse.

Unfortunately, I doubt you will see any changes to the weather data, as weather usually does not change that quickly. Let’s turn our attention to the Edit Location screen.

Editing the Locations

This screen will be where we can add a new city to our list, or remove an existing one. Here is what our final screen will look likeFigure 9-5.

Figure 9-5.The Add City dialog

Open the_locations.html_file and let’s add in our add city button after the<ion-content>. You should remove thepaddingattribute as well:

<
ion-content
>
<
button ion-button icon-left clear color="dark" item-left 
  (click)="addLocation()"
>
↵

<
ion-icon name="add"
>
<
/ion-icon
>
Add City
<
/button
>
<
/ion-content
>

For this button, we are applying the clear and dark styles to it. The clear attribute will remove any border or background from the button, while the dark attribute will color the icon with the dark color. We will have the click event call a function namedaddLocation.

After the<button>tag, we are going to add in our<ion-list>that will display the list of our saved locations:

<
ion-list
>
<
ion-list-header
>

    My Locations

<
/ion-list-header
>
<
ion-item *ngFor="let loc of locs"
>
<
button ion-button icon-left clear color="dark" 
   (click)="deleteLocation(loc)"
>
<
ion-icon name="trash"
>
<
/ion-icon
>
<
/button
>
{{loc.title}}

<
/ion-item
>
<
/ion-list
>

We are using the<ion-list-header>to act as our list header rather than just a regular header tag. Hopefully, the<ion-item>tag looks a bit familiar to you. It is just going to iterate over an array of locs and render out each location. Next, we set up a button that will allow us to delete a location. Again, we will use the clear and dark attributes for the button styling. The click event will call a function nameddeleteLocationthat we have yet to write. We will wrap up the code block by binding the text to the title value with{{loc.title}}.

With the HTML in place, we can focus on the changes we need to make in our code. We will start with_locations.ts_and add in the elements needed to get the template to work at a basic level.

We will again import ourCurrentLocclass:

import { CurrentLoc } from '../../interfaces/current-loc';

Within the class definition, we need to create ourlocsarray. This array is actually going to be the same content as ourpagesarray does in the_app.component.ts_file:

export class LocationsPage {
  locs: Array
<
{ title: string, component: any, icon: string, 
  loc?: CurrentLoc }
>
;

But whenever you find yourself repeating elements like this, you should always pause and consider if this is something that should be abstracted. In this case, our answer is going to be yes. Instead of listing out our array’s structure, we will move into the object to an interface and simply use it instead.

Create a new file named_weather-location.ts_inside the_interfaces_directory. Here we will define our WeatherLocation interface. Since thelocproperty uses theCurrentLocinterface, we need to make sure we import that module as well:

import { CurrentLoc } from './current-loc';

export interface WeatherLocation {
  title: string;
  component: any;
  icon: string;
  loc?: CurrentLoc;
}

The actual interface will define the location’s title, its page component, its icon, and an optional element of aCurrentLoctype.

With this interface created, switch back to_locations.ts_and import that interface:

import { WeatherLocation } from '../../interfaces/weather-location';

Then, we can update thelocsarray to be a properly typed array:

locs: Array
<
WeatherLocation
>
;

Next, let’s add the two placeholder functions for the adding and deleting of locations after the constructor:

deleteLocation(loc) {
  console.log('deleteLocation');
}

addLocation() {
  console.log('addLocation');
}

Let’s work on populating ourlocsarray with our saved places. We could simply copy over the pages array that is used in the side menu, and our template should render it out just fine. But any changes we make to ourlocsarray will not be reflected in thepagesarray. We need to move this data into something that both components can reference.

For this, we will create a new provider namedLocationsService. We will again make use the generate command in the Ionic CLI:

$ ionic g provider locationsService

Since this provider is not aimed at getting remote data, we are going to be replacing most of the generated template. The first thing we need to do is define our imports. We will needInjectablefrom Angular, as well as ourWeatherLocationinterface andWeatherPagecomponent:

import { Injectable } from '@angular/core';
import { WeatherLocation } from '../../interfaces/weather-location';
import { WeatherPage } from '../../pages/weather/weather';

Now we can define our service, thelocationsArray (properly typed toWeatherLocation), then initialize that array with our default locations:

export class LocationsServiceProvider {
  locations: Array
<
WeatherLocation
>
;

  constructor() {
    this.locations = [
      { title: 'Cape Canaveral, FL', component: WeatherPage, icon: 'pin', ↵
        loc: { lat: 28.3922, lon: -80.6077 } },
      { title: 'San Francisco, CA', component: WeatherPage, icon: 'pin', ↵
        loc: { lat: 37.7749, lon: -122.4194 } },
      { title: 'Vancouver, BC', component: WeatherPage, icon: 'pin', ↵
        loc: { lat: 49.2827, lon: -123.1207 } },
      { title: 'Madison, WI', component: WeatherPage, icon: 'pin', ↵
        loc: { lat: 43.0742365, lon: -89.381011899 } }
    ];
  }

We can wrap our service with the functions that we will need: one to get the locations, one to add to the locations, and one function to remove a location:

getLocations() {
    return Promise.resolve(this.locations);
}

removeLocation(loc:WeatherLocation) {
    let index = this.locations.indexOf(loc)
    if (index != -1) {
        this.locations.splice(index, 1);
    }
}

addLocation(loc: WeatherLocation) {
    this.locations.push(loc);
}

Returning back tolocations.ts, we can import this service into the component:

import { LocationsServiceProvider }  from ↵
  '../../providers/locations-service/locations-service';

We also need to add this module into our constructor, call that service’sgetLocationsmethod to get our default locations, and save the result into ourlocsarray:

constructor(public navCtrl: NavController, 
            public navParams: NavParams,
            public locationsService: LocationsServiceProvider) {
    locationsService.getLocations().then(res =
>
 {
      this.locs = res;
    });
  }

Saving the file, and navigating to the Edit Locations screen, you should now see our four default cities listed.

Deleting a City

Let’s implement the actualdeleteLocationfunction in the_locations.ts_file. We already have the stub function in place in our component. All we need to do is simply call theremoveLocationmethod on theLocationsService:

deleteLocation(loc:WeatherLocation) {
  this.locationsService.removeLocation(loc);
}

Adding a City

Adding a city is a bit more complex. If you recall, the Dark Sky weather service uses latitude and longitudes to look up the weather data. I doubt you will ask, “How is the weather in 32.715, –117.1625 ?” but rather, “How is the weather in San Diego?” To translate between a location and its corresponding latitude and longitude we need to use a geocoding service.

Using a Geocoding Service

For this app, we will use theGoogle Maps Geocoding API.

In order to use this API, we will need to be registered with Google as a developer and have an API key for our application. Go to theGoogle Maps API pageand follow the instructions to generate an API key for the geocoding API. Save this 40 character string, as we will need it shortly:

export class GeocodeServiceProvider {
  data: any;
  apikey:String = 
'YOUR-API-KEY-HERE'
;
  constructor(public http: Http) {
    this.data = null;
  }

  getLatLong(address:string) {
    if (this.data) {
      // already loaded data
      return Promise.resolve(this.data);
    }

    // don't have the data yet
    return new Promise(resolve =
>
 {
      this.http.get('https://maps.googleapis.com/maps/api/geocode/↵
      json?address='+encodeURIComponent(address)+'
&
key='+this.apikey)
        .map(res =
>
 res.json())
        .subscribe(data =
>
 {
          if(data.status === "OK") {
            resolve({name: data.results[0].formatted_address, location:{
                    latitude: data.results[0].geometry.location.lat,
                    longitude: data.results[0].geometry.location.lng
            }});
          } else {
            console.log(data);
            //reject
          }
        });
    });
  }
}

The first change is the new variable namedapikey. You will need to set its value to the key you just generated.

The constructor just sets the data variable tonull. The heart of this class is actually thegetLatLongmethod. This method accepts one parameter namedaddress.

The method will then make this request:

this.http.get('https://maps.googleapis.com/maps/api/geocode/json?↵
address='+encodeURIComponent(address)+'
&
key='+this.apikey)

Note that we have to use theencodeURIComponentmethod to sanitize the address string before we can make the call to the Google Maps Geocoding API.

Once the data is returned from the service, we can check if a named place was located using thedata.statusvalue. Then we can traverse the JSON, get the needed data, and resolve the Promise.

Now that we have a way to turn San Diego into 32.715, –117.1625, we can return back to_locations.ts_and finish ouraddLocationfunction.

We will need to import our new geocode service:

import { GeocodeServiceProvider } from ↵
  '../../providers/geocode-service/geocode-service';

Also include it with the constructor:

constructor(public navCtrl: NavController, 
            public navParams: NavParams,
            public locationsService: LocationsServiceProvider, 
            public geocodeService: GeocodeServiceProvider) {

Instead of using the Ionic Native Dialog plug-in to prompt our user to enter their point of interest, let’s use theAlertControllercomponent. Again, we need to import it from the proper library:

import { NavController, NavParams, AlertController } from 'ionic-angular';

One of the difficulties in using the Ionic Native Alert is the degree to which you can extend it. The alert also requires you to test your application either in a simulator or on-device. By using the standard Ionic Alert component, we can keep developing directly in our browser. However, unlike the Ionic Native dialog, there is quite a bit more JavaScript that you need to write. We will need to update our constructor to include theAlertController:

constructor(public navCtrl: NavController,
            public navParams: NavParams,
            public locationsService: LocationsServiceProvider,
            public geocodeService: GeocodeServiceProvider,
            public alertCtrl: AlertController) {

Here is the completedaddLocationmethod with the Alert component used:

addLocation() {
  let prompt = this.alertCtrl.create({
    title: 'Add a City',
    message: "Enter the city's name",
    inputs: [
      {
        name: 'title',
        placeholder: 'City name'
      },
    ],
    buttons: [
      {
        text: 'Cancel',
        handler: data =
>
 {
          console.log('Cancel clicked');
        }
      },
      {
        text: 'Add',
        handler: data =
>
 {
          console.log('Saved clicked');
        }
      }
    ]
  });

  prompt.present();
}

The key line of code not to forget is telling theAlertControllerto present our alert withprompt.present().

Now, we are not doing anything with any city that you might enter, so let’s add that code. Replace this line:

console.log('Saved clicked');

with:

if (data.title != '') {
  this.geocodeService.getLatLong(data.title).then(res =
>
 {
    let newLoc = { title: '', component: WeatherPage, icon: 'pin', ↵
                   loc: { lat: 0, lon: 0 } }
    newLoc.title = res.name;
    newLoc.loc.lat = res.location.latitude;
    newLoc.loc.lon = res.location.longitude;

    this.locationsService.addLocation(newLoc);
  });
}

We will also need to import ourWeathercomponent as well:

import { WeatherPage } from '../weather/weather';

So now we can take city name, parse it into a latitude and longitude value, and add it to our list of locations.

There are a couple of open issues I do want to point out with this code block. The first issue is that we are not handling the case of when the geocoding service fails to find a location. If you are up for that programming challenge, you need to add code that rejects the Promise. The second issue is handling when the geocoding service returns more than one answer. For example, if I enter Paris, do I want Paris, France or Paris, Texas? For this solution, you might want to look at the flexibility of theAlertcomponent to have radio buttons in a dialog.

Dynamically Updating the Side Menu

If you add a new location to your list and then use the side menu to view that location’s weather, you will see that it is not listed. That is because that list is still referencing the local version and not using the array inLocationsService. We will open the_app.component.ts_file and refactor it to enable this.

First, we need to import the service:

import { LocationsServiceProvider } from '../providers/locations-service/locations-service';

Next, we need to include in our constructor:

constructor(public platform: Platform, 
            public statusBar: StatusBar, 
            public splashScreen: SplashScreen, 
            public locationsService: LocationsServiceProvider) {
  this.initializeApp();
  this.getMyLocations();
}

ThegetMyLocationsfunction will get the data from theLocationsServiceprovider and then populate thepagesarray:

getMyLocations(){
  this.locationsService.getLocations().then(res =
>
 {
    this.pages = [
      { title: 'Edit Locations', component: LocationsPage, icon: 'create' },
      { title: 'Current Location', component: WeatherPage, icon: 'pin' }
    ];
    for (let newLoc of res) {
      this.pages.push(newLoc);
    }
  });
}

Save all the files and run$ ionic serveagain. The side menu should still show our initial list of places. If we add a new location, the Edit Locations screen is properly updated. However, the side menu is not showing our new location. Even though the side menu is getting its data from our shared provider, it does not know that the data has changed. To solve this issue, we need to explore two different options: Ionic Events and Observables. You might want to save a version of the progress so far, since each solution is a bit different.

Ionic Events

The first solution is to use IonicEventsto communicate the change in the dataset. According to the documentation:

Events is a publish-subscribe style event system for sending and responding to application-level events across your app.

Sounds pretty close to what we need to do. In the_locations.ts_file, we will need to import theEventscomponent fromionic-angular:

import {NavController, NavParams, AlertController, Events} from 'ionic-angular';

Within the constructor we need to have a reference to theEventsmodule:

constructor(public navCtrl: NavController,
            public navParams: NavParams,
            public locationsService: LocationsServiceProvider,
            public geocodeService: GeocodeService,
            public alertCtrl: AlertController,
            public events: Events) {

Now, in both thedeleteLocationandaddLocationfunctions, we just need to add this code to publish the event:

this.events.publish('locations:updated', {});

So thedeleteLocationbecomes:

deleteLocation(loc) {
  this.locations.removeLocation(loc);
  this.events.publish('locations:updated', {});
}

For theaddLocationfunction, it is added within the'Add'handler callback:

handler: data =
>
 {
  if (data.title != '') {
    this.geocode.getLatLong(data.title).then(res =
>
 {
      let newLoc = { title: '', component: WeatherPage, icon: 'pin', ↵
                     loc: { lat: 0, lon: 0 } }
      newLoc.title = res.name;
      newLoc.loc.lat = res.location.latitude;
      newLoc.loc.lon = res.location.longitude;

      this.locations.addLocation(newLoc);
      this.events.publish('locations:updated', {});
    });
  }
}

The parameters are the event name—in this case,locations:updated, and any data that needs to be shared. For this example, there is no additional data we need to send to the subscriber.

The subscriber function will be added to the_app.component.ts_file. First, we need to update our imports:

import { Nav, Platform, Events } from 'ionic-angular';

and pass it into our constructor:

constructor(public platform: Platform, 
            public locationsService: LocationsServiceProvider, 
            public events: Events) {

Within the constructor itself, we will add this code:

this.initializeApp();
this.getMyLocations();
events.subscribe('locations:updated', (data) =
>
 {
  this.getMyLocations();
});

This code will listen for alocations:updatedevent. Then it will call thegetMyLocationsfunction. In doing so, our array will be refreshed, and the side menu will be kept up to date.

Using IonicEventsmay not be the best solution to this problem, but it is worth knowing how to communicate events across an application.

Observables

You might be wondering if there was some other way for the data updates to propagate through our app without the need to manually send events. In fact, there is a solution available to us. One of the elements inside the RxJS library is Observables. From the documentation:

The Observer and Observable interfaces provide a generalized mechanism for push-based notification, also known as the observer design pattern. The Observable object represents the object that sends notifications (the provider); the Observer object represents the class that receives them (the observer).

In other words, the event notification system that we wrote with IonicEventscan be replaced. In our simple example using IonicEvents, we did not have to write a lot of code for our system to work. Now imagine a much more complex app and the messaging infrastructure that could quickly become a Gordian knot.

Now, to say that RxJS is a powerful and complex library is an understatement. But here are the basics of what we need to do within our app. First, we need to create an RxJS Subject, specifically a Behavior Subject. This will hold our data that we wish to monitor for changes. Next, we need to create the actual Observable that will watch our subject for changes. If it sees a change, it will broadcast the new set of data. The third and final part are the subscribers to ourObservable. Once they are bound together, our data will always be the latest.

We will start our refactoring in the_locations-service.ts_file. This is where the majority of the changes will occur as we shift our app from using IonicEventstoObservables.

As always, we need to import the needed modules:

import { Observable, BehaviorSubject } from 'rxjs/Rx';

Now, there are several types of Subject in the RxJS library. TheBehaviorSubjectis the best type for our needs, as it will send an update as soon as it gets data. Other types of Subjects require an additional method call to broadcast an update.

Next, we need to define the actualBehaviorSubjectandObservablewithin the class:

locations: Array
<
WeatherLocation
>
;
locationsSubject: BehaviorSubject
<
Array
<
WeatherLocation
>
>
 = 
new BehaviorSubject([]);
locations$: Observable
<
Array
<
WeatherLocation
>
>
 = 
this.locationsSubject.asObservable();

VARIABLE NAMING

The inclusion of the$after the variable name is a naming convention forObservabledata types. To learn more about Angular best practices, seeJohn Papa’s Style Guide.

We have only associated thelocations$with thelocationsSubjectby setting it tothis.locationsSubject.asObservable(). The locationsSubject knows nothing about our data in thelocationsarray. To solve this issue, simply pass in our locations array as the parameter to the locationsSubject’s next method:

this.locationsSubject.next(this.locations);

Upon doing this, ourObservablewill emit an update event, and all the references will be changed. Rather than repeating this call whenever ourlocationsarray changes, let’s wrap it in arefreshfunction:

refresh() {
  this.locationsSubject.next(this.locations);
}

Then in theconstructor,addLocation, andremoveLocationmethods, we can call this method after we have done whatever changes we needed to make to thelocationsarray. Here is the completed code:

import { Injectable } from '@angular/core';
import { WeatherLocation } from '../../interfaces/weather-location';
import { Weather } from '../../pages/weather/weather';
import { Observable, BehaviorSubject } from 'rxjs/Rx';

@Injectable()

export class LocationsServiceProvider {
  locations: Array
<
WeatherLocation
>
;
  locationsSubject: BehaviorSubject
<
Array
<
WeatherLocation
>
>
 = 
  new BehaviorSubject([]);
  locations$: Observable
<
Array
<
WeatherLocation
>
>
 = 
  this.locationsSubject.asObservable();

  constructor() {
    this.locations = [
      { title: 'Cape Canaveral, FL', component: Weather, icon: 'pin', ↵
        loc: { lat: 28.3922, lon: -80.6077 } },
      { title: 'San Francisco, CA', component: Weather, icon: 'pin', ↵
        loc: { lat: 37.7749, lon: -122.4194 } },
      { title: 'Vancouver, BC', component: Weather, icon: 'pin', ↵
        loc: { lat: 49.2827, lon: -123.1207 } },
      { title: 'Madison, WI', component: Weather, icon: 'pin', ↵
        loc: { lat: 43.0742365, lon: -89.381011899 } }
    ];
    this.refresh();
  }

  getLocations() {
    return Promise.resolve(this.locations);
  }

  removeLocation(loc) {
    let index = this.locations.indexOf(loc)
    if (index != -1) {
      this.locations.splice(index, 1);
      this.refresh();
    }
  }

  addLocation(loc) {
    this.locations.push(loc);
    this.refresh();
  }

  refresh() {
    this.locationsSubject.next(this.locations);
  }
}

With the service converted to using anObservable, we need to create the data subscribers to it. In_locations.ts_we now need to replace:

locationsService.getLocations().then(res =
>
 {
  this.locs = res;
});

with:

locationsService.locations$.subscribe( ( locs: Array
<
WeatherLocation
>
 ) =
>
 {
  this.locs = locs;
});

Now ourlocsarray will be automatically updated whenever the data changes in our service. Go ahead and remove the twothis.events.publishcalls.

A similar change is needed the_app.component.ts_file. First, let’s remove the event listener:

events.subscribe('locations:updated', (data) =
>
 {
  this.getMyLocations();
});

Next, we need to include theWeatherLocationinterface in the imports:

import { WeatherLocation } from '../interfaces/weather-location';

Finally, we can replace thegetMyLocationsfunction:

getMyLocations(){
  this.locationsService.getLocations().then(res =
>
 {
    this.pages = [
      { title: 'Edit Locations', component: LocationsPage, icon: 'create' },
      { title: 'Current Location', component: WeatherPage, icon: 'pin' }
    ];
    for (let newLoc of res) {
      this.pages.push(newLoc);
    }
  });
}

with:

getMyLocations(){
  this.locationsService.locations$.subscribe( ( locs: Array
<
WeatherLocation
>
 ) =
>

  {
      this.pages = [
        { title: 'Edit Locations', component: LocationsPage, icon: 'create' },
        { title: 'Current Location', component: WeatherPage, icon: 'pin' }
      ];
      for (let newLoc of locs) {
        this.pages.push(newLoc);
      }
    } );
}

With that, our weather app is now using RxJS Observables. Observables are a power solution when working with dynamic data. You would be well served to spend some time exploring their capabilities. Let’s now turn our attention to making our app a bit more visually pleasing.

Styling the App

With our app now functioning rather well, we can turn our attention to some visual styling. Included with the source code is a nice photograph of a partially cloudy sky. Let’s use this as our background image. Since our Ionic apps are based on HTML and CSS, we can leverage our existing CSS skills to style our apps. The only challenge in working with Ionic is uncovering the actual HTML structure that our CSS needs to properly target.

With the improved selector system within Ionic, targeting a specific HTML element is much easier. Let’s include a nice sky image to serve as our background for the Weather page. Open the_weather.scss_and add the following CSS:

page-weather {
  ion-content{
    background: url(../assets/imgs/bg.jpg) no-repeat center center fixed; 
    background-color: rgba(255, 255, 255, 0) !important;
    -webkit-background-size: cover;
    background-size: cover;
  }
}

WRITING SASS

The CSS that we are adding should be nested within the page-weather {}.

Now, let’s increase our base font size as well to make the text more readable:

ion-content{
  background: url(../assets/imgs/bg.jpg) no-repeat center center fixed; 
  -webkit-background-size: cover;
  background-size: cover;
  font-size: 24px;
}

However, the default black text is not really working against the sky and clouds. So we can adjust theion-coland its two children. We will change the color to white, center the text, and apply a drop shadow that has a little transparency:

ion-col, ion-col h1, ion-col p {
    color: #fff;
    text-align: center; 
    text-shadow: 3px 3px 3px rgba(0,0, 0, 0.4);
}

Now, adjust the current weather information’s text:

h1 {
    font-size: 72px;
}

p {
    font-size: 36px;
    margin-top: 0;
}

But, what about the header? It is looking a bit drab and out of place. Let’s add a new class,opaque, to the<ion-header>tag in the_weather.html_file.

In the_weather.scss_file we can apply a series of classes to give our header a more modern look. The first class we add will use the newbackdrop-filtermethod:

.opaque {
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
}

Unfortunately, backdrop-filter is only supported in Safari at this time. This means we need to also create a fallback solution:

.opaque .toolbar-background {
  background-color: rgba(#f8f8f8, 0.55);
}

With this pair of CSS classes, we have a much more current design style in place.

But there are still more elements to style on this screen, namely theRefreshercomponent. This one is a bit more complex than components we have styled before. Namely, it has a series of states that each need to be styled.

Let’s start with the two text elements; the pulling text and the refreshing text. Unfortunately, the current documentation does not list out the structure or style method. But with a little inspection with the Chrome Dev Tools, these elements did already have CSS classes applied to them. So we can just add the following:

.refresher-pulling-text, .refresher-refreshing-text  {
    color:#fff;
}

The arrow icon is also easily styled using:

.refresher-pulling-icon {
    color:#fff;
}

But what about the spinner? The dark circles aren’t really standing out against our background. This element is a little tricky to style as well. The spinner is actually an SVG element. This means we cannot change it just by changing the CSS of the<ion-spinner>, but instead we need to modify the values within the SVG. One thing to note is that some of the CSS properties on an SVG element have different names. For example, SVG uses the termstroke_instead of_border, andfill_instead of_background-color.

If you are using the circles option for yourrefreshingSpinnerattribute, then the styling is:

.refresher-refreshing .spinner-circles circle{
    fill:#fff;
  }

But, say you decided to use crescent as your spinner, then the styling would be:

 .refresher-refreshing .spinner-crescent circle{
    stroke:#fff;
  }

There is one last item that we should change. You probably did not see that while the Refresher is visible, there was a solid 1-pixel white line between the Refresher and the content. With our full background image, this doesn’t quite work for this design. Again, with some inspection with Chrome Dev Tools, the CSS was found where this attribute was set. So, we can now override it with:

.has-refresher 
>
 .scroll-content {
    border-width: 0;
}

Here is the full_weather.scss_code:

page-weather {
  ion-content{
    background: url(../assets/imgs/bg.jpg) no-repeat center center fixed; 
    -webkit-background-size: cover;
    background-size: cover;
    font-size: 24px;
  }

  ion-col, ion-col h1, ion-col p {
    color: #fff;
    text-align: center; 
    text-shadow: 3px 3px 3px rgba(0,0, 0, 0.4);
  }

  h1 {
    font-size: 72px;
  }

  p {
    font-size: 36px;
    margin-top: 0;
  }

  .opaque {
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
  }

  .opaque .toolbar-background {
    background-color: rgba(#f8f8f8, 0.55);
  }

  .refresher-pulling-text, .refresher-refreshing-text  {
    color:#fff;
  }

  .refresher-pulling-icon {
    color:#fff;
  }

  .refresher-refreshing .spinner-circles circle{
    fill:#fff;
  }

  .has-refresher 
>
 .scroll-content {
    border-top-width: 0;
  }
}

See the style weather app inFigure 9-6.

Figure 9-6.Our styled Ionic weather app

With that our main weather page is looking rather nice.But we can add one more touch to the design. How about a nice icon as well?

Add a Weather Icon

The Dark Sky data set actually defines an icon value, and the Ionicons also support several weather icons as wells.Table 9-1maps each icon.

Dark Sky name Ionicon name
clear-day sunny
clear-night moon
rain rainy
snow snow
sleet snow
wind cloudy
fog cloudy
cloudy cloudy
partly-cloudy-day partly-sunny
partly-cloudy-night cloudy-night

We don’t have a complete one-to-one mapping, but it is close enough. To resolve the mapping between the Dark Sky name and the Ionicon name, we will create a customPipefunction. If you recall,Pipesare functions that transform data in a template.

From our terminal, we can use theionic generatecommand to scaffold our pipe for us:

$ ionic generate pipe weathericon

This will create a new directory namedpipes, and a new file named_weathericon.ts._Here is the stock code that Ionic will generate for us:

import { Pipe, PipeTransform } from '@angular/core';

/**
 * Generated class for the WeathericonPipe pipe.
 *
 * See https://angular.io/docs/ts/latest/guide/pipes.html for more info on
 * Angular Pipes.
 */
@Pipe({
  name: 'weathericon',
})
export class WeathericonPipe implements PipeTransform {
  /**
   * Takes a value and makes it lowercase.
   */
  transform(value: string, ...args) {
    return value.toLowerCase();
  }
}

Let’s replace the code within thetransformfunction. The goal of this function will be to take the Dark Sky icon string and find the corresponding Ionicon name. Here is the revisedPipe:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'weathericon',
})
export class WeathericonPipe implements PipeTransform {
  transform(value: string, args: any[]) {
    let newIcon: string = 'sunny';
    let forecastNames: Array
<
string
>
 = ["clear-day", "clear-night", "rain", ↵
"snow", "sleet", "wind", "fog", "cloudy", "partly-cloudy-day", "partly-cloudy-night"];
    let ioniconNames: Array
<
string
>
 = ["sunny", "moon", "rainy", ↵
"snow", "snow", "cloudy", "cloudy", "cloudy", "partly-sunny", "cloudy-night"];
    let iconIndex: number = forecastNames.indexOf(value);
    if (iconIndex !== -1) {
      newIcon = ioniconNames[iconIndex];
    }

    return newIcon;
  }
}

In our_app.module.ts_file, we will automatically updated for us.

Finally, the last change is to markup in the_weather.html_file, which will become:

<
ion-col col-12
>
<
h1
>
 {{currentData.temperature | number:'.0-0'}}
&
deg;
<
/h1
>
<
p
>
<
ion-icon name="{{currentData.icon | weathericon}}"
>
<
/ion-icon
>
↵
 {{currentData.summary}}
<
/p
>
<
/ion-col
>

Figure 9-7shows the Ionic weather app after applying the weather Ionicon.

Figure 9-7.Ionic weather app with a weather Ionicons applied

That really does finish off the look of this screen. Feel free to continue to explore styling the side menu and the locations page on your own.

Next Steps

Our weather app still has some things that can be improved upon. Most notably, our custom cities are not saved. For something as simple as a list of cities, using Firebase might be overkill. Some options you might consider would be either local storage or using the Cordova File plug-in to read and write a simple datafile. The choice is up to you.

Another challenge you might consider would be to introduce dynamic backgrounds. You could use a Flickr API to pull an image based on the location or on the weather type.

One more challenge you could take on is to support changing the temperature units.

Summary

With this application, we explored how the sidemenu template functions. You were introduced to using Pipes to transform data within a template. We fetched data from an external source, and learned how to use the proxy system in Ionic to address any CORS issues. Finally we looked at both Ionic Events and Observables as methods to update our UI dynamically.

results matching ""

    No results matching ""