© 2018, Drifty, Inc. All rights reserved. Reproduction and distribution of this material is prohibited.
<ion-header>
<ion-navbar>
<ion-title>I am the title</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
I am the page content.
</ion-content>npm install -g ionic@latestionic startionic labionic servenpm install -g ionic@latest
ionic start
ionic serveionic lab
ionic serve
ionic lab
You’ll need a few things, many of which you probably already have. Note that you may need administrator rights to install software and update files.
You need your own computer in class, set up according to the instructions given here.
The ionic command-line interface uses Node, and you’ll use the Node Package Manager (npm) frequently as you code. Npm is included with Node.
To see if you have node, go to the command line and enter
node --version If you get a “command not found” error, you need to install node.js at https://nodejs.org/en/
When you’re finished, run node --version again to verify that Node.js is installed.
Some labs use Ionic Pro, which requires git.
To see if you have git, use a terminal window and enter
git version If you get a “command not found” error, you need to install Git at https://git-scm.com/downloads
If you’re interested, here’s a nice guide that discusses installing git on Windows (with a nice section on SSH keys): http://guides.beanstalkapp.com/version-control/git-on-windows.html#installing-ssh-keys
When you’re finished, run git version again to verify that Git is installed.
You need a folder named IonicTraining. You’ll do all your coding in that folder. On a Mac, we recommend placing that at the root of your user ~ directory. If you’re on Windows, we suggest placing it at the root of your C:\ drive.
If you don't have Chrome, please download and install it. https://www.google.com/chrome/
Chrome isn’t required to do Ionic development, but it has nice debugging tools. Lab instructions assume you’re using Chrome.
Ionic code can be written using any plain text editor, but it’s best to use a source code editor or IDE. Visual Studio Code has good out-of-the-box TypeScript support, but you’re free to use the editor of your choice. Here are some suggestions.
Whichever editor you use, you should install some kind of source code beautifier. You might also look for editor plugins designed to support Ionic and Angular programming.
You need the Ionic cli, installed globally. To do that, open a terminal window and enter this.
If you're using a Mac or UNIX, you may need to run the command via as a super user, via sudo.
You'll use a few Ionic Pro features during class, so you need an account.
You may already have an account, but if you don't, please visit https://dashboard.ionicframework.com/signup and sign up. You only need the free starter account for class.
Depending on whether you want to build for Android or iOS, follow the Android or iOS platform guides.
ionic lab
ionic serve
In this lab you'll use the Ionic command-line interface to generate a starter app.
Open a terminal window and enter npm install -g ionic@latest.
If you're on a Mac, you may not have permissions to do the install. In that
case, run the command as a superuser via sudo npm install -g ionic@latest.
Open a terminal window and enter ionic start itunes blank.
You'll be asked if you want to Would you like to integrate your new app with Cordova to target native iOS and Android? Answer n.
You'll also be asked Install the free Ionic Pro SDK and connect your app?. Answer n.
We'll try native and pro features later on.
After generating the starter app, there will be a new folder named itunes
containing your new app.
Using a code editor, inspect the app. It contains these files and folders:
srcapp/app.component.tsapp.htmlapp.module.tsapp.scssmain.tspages/home/home.htmlhome.scsshome.tstheme/index.html
The app folder is mostly boilerplate code that you rarely need to modify
(other than configuring newly created components and providers in the app module.)
app/main.ts is the standard Angular bootstrap code.app/app.module.ts is a fairly standard Angular module,
set up to use the components in the starter app.app directory — app.component.ts,
app.html and app.scss — define the Ionic top-level component, an
<ion-nav>. This component containes the page defined in pages/home.The pages folder holds the pages in the app. There's only one now — pages/home.
src/index.html is the starting point of the app. It load some scripts and
CSS, and in the body, contains <ion-app></ion-app>, which is the root element
for an Ionic app. You rarely need to modify the contents of index.html.
Use your code editor to open src/pages/home/home.html. It has an <ion-header>
and an <ion-content>. A page may only have a single header and content. The header
typically holds an <ion-navbar> — which holds the back button after you
push a new page on the navigation stack — and an <ion-toolbar>, if needed.
The <ion-content> has a padding attribute. Ionic has several pre-defined
attributes for modifying and placing elements.
Use a terminal window and navigate to the itunes folder and run the
cli command ionic serve. This builds your app, then starts a server at port
8100, and opens a browser window running the app. As you edit your app the
browser window automatically reloads.
Note that occasionally (but rarely), code changes may not be detected properly and the running app will be out of date. If that happens, simply manually refresh the browser window.
When you're developing code, it's common to run the app with the debugger window open, and docked on the right.
Edit src/pages/home.html and replace the <ion-content> with this:
<ion-content padding><button ion-button>Push</button></ion-content>
This simply shows a button labeled Push in the content area. There are many ways of styling buttons, which we'll cover later.

Ionic apps navigate from page to page by pushing a page onto a stack. The top page is seen by the user. If the top page is popped (removed), the new top item is seen, and so forth, until the user reaches the root page.
There are two ways to push pages on the stack:
navPush propertypush on the NavControllerTo try it declaratively, edit src/page/home/home.html and replace the button
with this:
<button ion-button [navPush]="foo">Push</button>
The code results in the component's foo value being pushed on the stack.
But you don't have a foo property yet, so your code editor's linter will flag foo as undefined.
We need to set foo to some page, and right now we only have one: the home page.
Edit src/page/home/home.ts and in the class add a member field, foo,
set to HomePage:
import { Component } from '@angular/core';import { NavController } from 'ionic-angular';@Component({selector: 'page-home',templateUrl: 'home.html'})export class HomePage {foo = HomePage;constructor(public navCtrl: NavController) {}}
Save your changes, then try it out: As you click on Push, it pushes a new instance on the stack and puts a back button on the navigation bar. Clicking the back button pops the current page off the stack.

NavController
The NavController class has push and pop methods, as well as methods for
inspecting the stack.
For example, it has a getViews() method that returns the views on the stack.
Try it out by using this code in src/pages/home/home.ts:
import { Component } from '@angular/core';import { NavController, ViewController } from 'ionic-angular';@Component({selector: 'page-home',templateUrl: 'home.html'})export class HomePage {foo = HomePage;views: ViewController[];constructor(public navCtrl: NavController) {this.views = this.navCtrl.getViews();}}
As you can see, getViews() returns an array of ViewController (which has
also been added to the import statement).
To see how deep we've drilled onto the stack, edit src/pages/home/home.html
and replace the <ion-title> with this:
<ion-title>{{(views.length===1)?'1 item':views.length + ' items'}} in the stack.</ion-title>
(The ternary operator is a little awkward. We might have used <ng-pluralize>,
or created our own pipe for that.)
To push procedurally, we need a click event on the button, and from the
handler, use the nav controller push() method.
Edit src/pages/home/home.html and replace the <button> with this:
<button ion-button (click)="pushClick($event)">Push</button>
This code adds a click event on the button. As before, you'll get an error
message because the pushClick() method doesn't exist yet.
Edit src/pages/home/home.ts and replace the code with this:
import { Component } from '@angular/core';import { NavController, ViewController } from 'ionic-angular';@Component({selector: 'page-home',templateUrl: 'home.html'})export class HomePage {foo = HomePage;views: ViewController[];constructor(public navCtrl: NavController) {this.views = this.navCtrl.getViews();}pushClick(event: MouseEvent) {this.navCtrl.push(HomePage);console.log(event);}}
Save and look at your app. As before, pressing the button pushes the view, and
you should be able verify that the new code is being used by looking at the
console — you should see the MouseEvent being logged each time you
click Push.
In this lab you use ionic start to create a starter app. Then you
explored pushing a new page onto the navigation stack:
<button ion-button [navPush]="foo">Push</button>
push() on the injected NavController
Lists items can contain a range of component types or plain HTML
<ion-item> elements can be hard coded
But more typically, the data is in an array
In this lab you'll change the home page to be a list. Initially, the list will use hard-coded data. Later, you'll use a service provider to fetch data from Apple.
home.html a listEdit pages/home/home.html and replace its content with this code:
<ion-header><ion-navbar><ion-title>Top 50 iTunes Music Videos</ion-title></ion-navbar></ion-header><ion-content padding><ion-list><ion-item>Music video</ion-item><ion-item>Music video</ion-item></ion-list></ion-content>
Since you have ionic serve already running, you'll see the app changing
right away.

Edit pages/home/home.ts. It has some code from the last lab that
you no longer need. Completely replace its contents with this code.
import { Component } from "@angular/core";import { NavController, ViewController } from "ionic-angular";@Component({selector: "page-home",templateUrl: "home.html"})export class HomePage {constructor(public navCtrl: NavController) {}}
Rather than hard-coding the two items, create some test data,
and use an *ngFor to show the <ion-item> elements.
First, edit pages/home/home.ts and add a class property:
tunes = [{thumbnail:"http://is2.mzstatic.com/image/thumb/Video128/v4/e9/58/89/e95889a9-deeb-9b41-ed50-0be244289a50/191773963576_1_1.jpg/100x100bb-85.jpg",artist: "BTS",title: "MIC Drop"},{thumbnail:"http://is2.mzstatic.com/image/thumb/Video128/v4/05/d4/03/05d40316-e624-e7a3-f8c6-61f76cd1e0c5/8864468205830101VIC.jpg/100x100bb-85.jpg",artist: "Camila Cabello",title: "Havana"},{thumbnail:"http://is5.mzstatic.com/image/thumb/Video128/v4/d2/4e/31/d24e3191-e0d5-be9b-7680-6dcf603be7e2/080688999995_USMVC1700038.jpg/100x100bb-85.jpg",artist: "for KING & COUNTRY",title: "Little Drummer Boy"}];
This is mocked up iTunes data. Later you'll get the data from a service provider.
Then edit pages/home/home.html and replace the contents of
the <ion-list> with an <ion-item> which uses an *ngFor,
that loops over the elements in the tunes array.
<ion-list><ion-item *ngFor="let tune of tunes">{{tune.title}} — {{tune.artist}}</ion-item></ion-list>
Note the code within the double braces. As you probably know, template code may contain expressions embedded in double curly
braces. The expression may be somethin like 1+1. The
expression can use component variables, or a variable defined
in an emcompassing *ngFor, which is what's happening here.
The code loops over the tunes array (definied in the component class) and assigns subsequent items to the variable
tune, which is local to the loop. The variable can be named
anything — like foo or moose — although a
logical name is best.

You hard-coded the title to Top 50 iTunes Music Videos. But you're showing data from an array that only has three items.
Edit pages/home/home.html and change the title to use an double-bracked expression that shows the length of the array.
After the change, the title should show up as Top 3 iTunes Music Videos.
Remember that JavaScript arrays have a length proprty. For example,
console.log(['a','b','c'].length); // logs 3
So in the title, just use dot notation to show the length of the tunes array.
When the user taps an item, you'll show the preview page. You don't have the preveiw page yet, but you can still detect the click event.
Edit pages/home/home.html and add a click event on the
<ion-item> and have it run the method onItemClick(tune)
<ion-item *ngFor="let tune of tunes" (click)="onItemClick(tune)">
Initially, your linter will complain that the onItemClick
method doesn't exist. Edit pages/home/home.ts and add
the method.
onItemClick(tune) {console.log(`You clicked on ${tune.title}`);}
Now go to the app running in your browser, and click on an item. You should see the title logged on the console.

Note that in Ionic version 3, clickable items should be
buttons, with the ion-item directive. In other words,
the item would be coded like this:
<button ion-item *ngFor="let tune of tunes">{{tune.title}} — {{tune.artist}}</button>
In this lab you changed the home page to use an <ion-list>.
The list shows one <ion-item> for every song in a tunes array. For now, that array is hard coded.
You're also detecting when the user taps on an item. Later you'll use that method to show the music video preview page.
// Some view
onClick(item){
this.navCtrl.push(MyClass, {foo: 'bar'});
}
// The class being pushed
export class MyClass{
constructor(public navParams: NavParams){
// Logs 'bar'
console.log( this.navParams.get('foo') };
// data references the whole object
console.log( this.navParams.data );
}
}
In this lab you'll use the Ionic command-line interface to generate the preview page, then use the list item click event to show it to the user.
Initially, the preview page will simply show some information
about the music video — later on you'll add the <video> tag.
The Ionic cli provides a way to generate a page. You specify a name, and the cli generates the page.
Use a terminal window, and navigate to your app's root directory. Then run this command
ionic generate page previewYou should see a new set of files created:
pages
preview
preview.html
preview.module.ts
preview.scss
preview.tsThe cli does not update the app module in app/app.module.ts.
You have a few options for doing that:
1. Import the page and update the corresponding module
This option is a little complicated, because you have
to edit the import, declarations:[] and
entryComponents:[].
2. Import the page's module
With this option, you import the preview page's module,
and update the @NgModule imports:[] accordingly. That's
a little simpler. And another benefit is if you add other preview-related components, you can add them to the page's module, rather than having to edit the app module.
3. Omit it from the app module, and use dynamic loading
Dynamic loading is another good technique. You'll get hands-on with dynamic loading later.
For the preview page, you'll use option two.
Edit app/app.module.ts and import the preview page's module.
import { PreviewPageModule } from "../pages/preview/preview.module";Then add PreviewPageModule to the @NgModule imports array.
At this point, your app should run without error, but you still aren't actually using the new preview page.
Edit pages/home/home.ts and import the PreviewPage class.
import { PreviewPage } from "../preview/preview";Then modify the onItemClick method to push it onto the navigation stack.
onItemClick(tune) {
this.navCtrl.push(PreviewPage);
console.log(`You clicked on ${tune.title}`);
}If you run the app, you should successfully push onto the preview page. If you look at pages/preview/preview.html you'll see that it sets the title to Preview, but the page
content is empty.
The preview page will needs to show the song title and
use other information from the tune. Data passed to the
push method is wrapped in a NavParams object.
To do that, first, edit pages/home/home.ts and change the push call in onItemClick.
this.navCtrl.push(PreviewPage, tune);Recall that the function has a tune parameter, set to the selected song. By adding it as the second parameter to push, the tune is passed to the constructor of PreviewPage via
an injected NavParams.
Now edit pages/preview/preview.ts and add a class
property tune, and initialize it in the
constructor via the injected NavParams. Remember: the data
is passed in this.navParams.data.
Now you're free to use that data on the preview page.
Edit pages/preview/preview.html and use the tunes
object.
First, set the <ion-title> to tune.title (using
a double-braces expresion).
Then set the <ion-content> to show the title and artist name, using tune.title and tune.artist.
Save your changes, then use your browser to click on an item. You should see the preview page pushed, with the song title in the header, and the title and artist in the content area.

In this lab you changed the home page to use an <ion-list>. The list shows one <ion-item> for every song in a tunes array. For now, that array is hard coded.
You also used the Ionic cli to generate the preview page. On the home page, when the user clicks a list item, the preview page is pushed onto the navigation stack, passing the selected song.
The preview page shows the song title and artist. Later, you'll add
a <video> element and show the music video preview.
If you get stuck, here's the code as of the end of the lab.
app/app.module.tsimport { BrowserModule } from "@angular/platform-browser";
import { ErrorHandler, NgModule } from "@angular/core";
import { IonicApp, IonicErrorHandler, IonicModule } from "ionic-angular";
import { SplashScreen } from "@ionic-native/splash-screen";
import { StatusBar } from "@ionic-native/status-bar";
import { MyApp } from "./app.component";
import { HomePage } from "../pages/home/home";
import { PreviewPageModule } from "../pages/preview/preview.module";
import { PrefixNot } from "@angular/compiler";
@NgModule({
declarations: [MyApp, HomePage],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
PreviewPageModule
],
bootstrap: [IonicApp],
entryComponents: [MyApp, HomePage],
providers: [
StatusBar,
SplashScreen,
{ provide: ErrorHandler, useClass: IonicErrorHandler }
]
})
export class AppModule {}pages/home/home.tsimport { Component } from "@angular/core";
import { NavController } from "ionic-angular";
@Component({
selector: "page-home",
templateUrl: "home.html"
})
export class HomePage {
tunes: Tune[] = [];
constructor(
public navCtrl: NavController
) {}
onItemClick(tune) {
this.navCtrl.push(PreviewPage);
console.log(`You clicked on ${tune.title}`);
}
}pages/preview/preview.html<ion-header>
<ion-navbar>
<ion-title>{{tune.title}}</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
{{tune.title}} — {{tune.artist}}
</ion-content>pages/preview/preview.ts
```javascript
import { Component } from "@angular/core";
import { IonicPage, NavController, NavParams } from "ionic-angular";
@IonicPage()
@Component({
selector: "page-preview",
templateUrl: "preview.html"
})
export class PreviewPage {
tune;
constructor(public navCtrl: NavController, public navParams: NavParams) {
this.tune = this.navParams.data;
}
ionViewDidLoad() {
console.log("ionViewDidLoad PreviewPage");
}
}ionViewCanEnter()
ionViewDidLoad()
ionViewWillEnter()
ionViewDidEnter()
ionViewCanLeave()
ionViewWillLeave()
ionViewDidLeave()
In this lab you'll create a provider that fetches iTunes data.
Doing that is a little complicated, so you'll implement the code in three phases:
In a terminal window, navigate to your app root and run this command.
ionic generate provider itunes
This command generates the file providers/itunes/itunes.ts, and automatically updates the app module to add the class to the provider array.
Edit providers/itunes/itunes.ts and replace its contents with the following code.
import { Injectable } from "@angular/core";@Injectable()export class ItunesProvider {constructor() {console.log("Hello ItunesProvider Provider");}get() {return new Promise<{}[]>((resolve, reject) => {resolve([{thumbnail:"http://is2.mzstatic.com/image/thumb/Video128/v4/e9/58/89/e95889a9-deeb-9b41-ed50-0be244289a50/191773963576_1_1.jpg/100x100bb-85.jpg",artist: "BTS",title: "MIC Drop"},{thumbnail:"http://is2.mzstatic.com/image/thumb/Video128/v4/05/d4/03/05d40316-e624-e7a3-f8c6-61f76cd1e0c5/8864468205830101VIC.jpg/100x100bb-85.jpg",artist: "Camila Cabello",title: "Havana"},{thumbnail:"http://is5.mzstatic.com/image/thumb/Video128/v4/d2/4e/31/d24e3191-e0d5-be9b-7680-6dcf603be7e2/080688999995_USMVC1700038.jpg/100x100bb-85.jpg",artist: "for KING & COUNTRY",title: "Little Drummer Boy"}]);});}}
The code gets rid of the HTTP import that was in the generated code — you'll
be using a different class for fetching iTunes data.
The code also has a get
method that returns a promise, which returns the same hard-coded data you're
currently using in pages/home/home.ts. The promise is typed
to return an array of objects — you'll make it more strongly typed in
a later lab.
Note that the provider uses the @Injectible class decorator. This means
you can inject it in classes that use it. @Injectable is covered in
detail later in class.
You need to change the code to use the new provider. Currently, the tunes
class member is hard coded to an array. You need to change that to use
the new provider.
Edit pages/home/home.ts and do three things:
First, import the provider.
import { ItunesProvider } from "../../providers/itunes/itunes";
Second, inject it in the constructor
constructor(public iTunesProvider: ItunesProvider,public navCtrl: NavController){}
Third, change the tunes declaration to no longer use the hard-coded array:
tunes = [];
Finally, get the data when the page is first loaded. To do that, you'll
use the lifecycle method ionViewDidLoad.
ionViewDidLoad() {this.iTunesProvider.get().then(data => (this.tunes = data));}
In theory you could have fetched the data in the constructor, but as a best practice, constructors are used to define injected classes, and their method bodies are empty. To initialize values, you use whatever lifecycle method meets your needs. For more information, bookmark this article and read it some day: Flaw: Constructor does Real Work
Save your changes and verify that everything works.
[{
"im:artist": {
"label": "Justin Bieber",
},
"title": {
"label": "What Do You Mean? - Justin Bieber"
},
...
https://jsbin.com/bebuwek/2/edit?js,output
In this lab you'll continue to work on the provider, by processing data exactly like it will be returned from iTunes.
The hard-coded data is structured the way the view likes it, but it doesn't match what you'll get from the iTunes feed. This was discussed in lecture.That data looks like this: https://itunes.apple.com/us/rss/topmusicvideos/limit=50/json
As you saw in lecture, the data is a mess, and you need code to map one of those entries to the properties needed in the view. The view needs:
The code needs to be able to process iTunes data. So before actually fetching that dynamically, you'll hard code it for now.
Create the file providers/itunes/test-data.ts with this code. The code is simply a copy of actual iTunes data.
It scrolls far to the right, so be careful to get the
whole thing as you copy and paste.
let data = {"feed":{"author":{"name":{"label":"iTunes Store"}, "uri":{"label":"http://www.apple.com/itunes/"}}, "entry":[{"im:name":{"label":"Perfect Symphony (with Andrea Bocelli)"}, "rights":{"label":"℗ 2017 Asylum Records UK, a division of Atlantic Records UK, a Warner Music Group company."}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video128/v4/9e/c9/ec/9ec9ec3e-caf7-4979-cd88-9ce0007d0327/GB1301700803.sca1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is2.mzstatic.com/image/thumb/Video128/v4/9e/c9/ec/9ec9ec3e-caf7-4979-cd88-9ce0007d0327/GB1301700803.sca1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is1.mzstatic.com/image/thumb/Video128/v4/9e/c9/ec/9ec9ec3e-caf7-4979-cd88-9ce0007d0327/GB1301700803.sca1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Ed Sheeran", "attributes":{"href":"https://itunes.apple.com/us/artist/ed-sheeran/183313439?uo=2"}}, "title":{"label":"Perfect Symphony (with Andrea Bocelli) - Ed Sheeran"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/perfect-symphony-with-andrea-bocelli/1326184705?uo=2"}},{"im:duration":{"label":"30045.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video128/v4/1a/4c/88/1a4c8899-21c5-bf16-21f5-d181291a76c3/mzvf_7748454052966646394.640x362.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/perfect-symphony-with-andrea-bocelli/1326184705?uo=2", "attributes":{"im:id":"1326184705"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1763", "term":"Classical Crossover", "scheme":"https://itunes.apple.com/us/genre/music-videos-classical-classical-crossover/id1763?uo=2", "label":"Classical Crossover"}}, "im:releaseDate":{"label":"2017-12-15T00:00:00-07:00", "attributes":{"label":"December 15, 2017"}}},{"im:name":{"label":"Perfect"}, "rights":{"label":"℗ 2017 Asylum Records UK, a division of Atlantic Records UK, a Warner Music Group company."}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is2.mzstatic.com/image/thumb/Video128/v4/6e/2f/d9/6e2fd933-4540-ae71-fa1a-75be3aebb7ea/GB1301700433.sca1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is3.mzstatic.com/image/thumb/Video128/v4/6e/2f/d9/6e2fd933-4540-ae71-fa1a-75be3aebb7ea/GB1301700433.sca1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Video128/v4/6e/2f/d9/6e2fd933-4540-ae71-fa1a-75be3aebb7ea/GB1301700433.sca1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Ed Sheeran", "attributes":{"href":"https://itunes.apple.com/us/artist/ed-sheeran/183313439?uo=2"}}, "title":{"label":"Perfect - Ed Sheeran"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/perfect/1313284807?uo=2"}},{"im:duration":{"label":"30045.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video118/v4/15/b3/30/15b33020-62d0-d293-8290-58feea0dc8dd/mzvf_7925914164347605191.640x354.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/perfect/1313284807?uo=2", "attributes":{"im:id":"1313284807"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2017-11-10T00:00:00-07:00", "attributes":{"label":"November 10, 2017"}}},{"im:name":{"label":"Finesse (Remix) [feat. Cardi B]"}, "rights":{"label":"℗ 2018 Atlantic Recording Corporation for the United States and WEA International Inc. for the world outside of the United States. A Warner Music Group Company"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is3.mzstatic.com/image/thumb/Video118/v4/15/a0/3c/15a03c72-ed89-162a-a52f-696634915b09/dj.hqecwkgk.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is4.mzstatic.com/image/thumb/Video118/v4/15/a0/3c/15a03c72-ed89-162a-a52f-696634915b09/dj.hqecwkgk.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Video118/v4/15/a0/3c/15a03c72-ed89-162a-a52f-696634915b09/dj.hqecwkgk.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Bruno Mars", "attributes":{"href":"https://itunes.apple.com/us/artist/bruno-mars/278873078?uo=2"}}, "title":{"label":"Finesse (Remix) [feat. Cardi B] - Bruno Mars"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/finesse-remix-feat-cardi-b/1332157721?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video118/v4/2e/fe/23/2efe2307-a2cc-f29f-57fa-a37007e78fa5/mzvf_7690325983878593531.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/finesse-remix-feat-cardi-b/1332157721?uo=2", "attributes":{"im:id":"1332157721"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1615", "term":"R&B/Soul", "scheme":"https://itunes.apple.com/us/genre/music-videos-r-b-soul/id1615?uo=2", "label":"R&B/Soul"}}, "im:releaseDate":{"label":"2018-01-05T00:00:00-07:00", "attributes":{"label":"January 5, 2018"}}},{"im:name":{"label":"Say Something (feat. Chris Stapleton)"}, "rights":{"label":"℗ (C) 2018 RCA Records, a division of Sony Music Entertainment"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is2.mzstatic.com/image/thumb/Video118/v4/6d/60/24/6d602415-8aee-f017-28df-eab75e151b84/8864469558340101.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is3.mzstatic.com/image/thumb/Video118/v4/6d/60/24/6d602415-8aee-f017-28df-eab75e151b84/8864469558340101.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is5.mzstatic.com/image/thumb/Video118/v4/6d/60/24/6d602415-8aee-f017-28df-eab75e151b84/8864469558340101.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Justin Timberlake", "attributes":{"href":"https://itunes.apple.com/us/artist/justin-timberlake/398128?uo=2"}}, "title":{"label":"Say Something (feat. Chris Stapleton) - Justin Timberlake"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/say-something-feat-chris-stapleton-official-video/1339773481?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video62/v4/7c/1f/14/7c1f1418-7702-25e3-81f3-69d4f41ddee2/mzvf_8264523775782886530.640x318.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/say-something-feat-chris-stapleton-official-video/1339773481?uo=2", "attributes":{"im:id":"1339773481"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2018-01-26T00:00:00-07:00", "attributes":{"label":"January 26, 2018"}}},{"im:name":{"label":"Havana (feat. Young Thug)"}, "rights":{"label":"℗ (C) 2017 Simco Ltd. under exclusive license to Epic Records, a division of Sony Music Entertainment"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is2.mzstatic.com/image/thumb/Video128/v4/05/d4/03/05d40316-e624-e7a3-f8c6-61f76cd1e0c5/8864468205830101VIC.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/05/d4/03/05d40316-e624-e7a3-f8c6-61f76cd1e0c5/8864468205830101VIC.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is2.mzstatic.com/image/thumb/Video128/v4/05/d4/03/05d40316-e624-e7a3-f8c6-61f76cd1e0c5/8864468205830101VIC.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Camila Cabello", "attributes":{"href":"https://itunes.apple.com/us/artist/camila-cabello/935727853?uo=2"}}, "title":{"label":"Havana (feat. Young Thug) - Camila Cabello"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/havana-feat-young-thug/1299575340?uo=2"}},{"im:duration":{"label":"30045.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video128/v4/60/3f/d4/603fd473-f1bf-1a60-76ac-0dfd5939da37/mzvf_4461752779223323573.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/havana-feat-young-thug/1299575340?uo=2", "attributes":{"im:id":"1299575340"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2017-10-25T00:00:00-07:00", "attributes":{"label":"October 25, 2017"}}},{"im:name":{"label":"MIC Drop (Steve Aoki Remix)"}, "rights":{"label":"℗ 2017 Bighit Entertainment"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/e9/58/89/e95889a9-deeb-9b41-ed50-0be244289a50/191773963576_1_1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is3.mzstatic.com/image/thumb/Video128/v4/e9/58/89/e95889a9-deeb-9b41-ed50-0be244289a50/191773963576_1_1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is2.mzstatic.com/image/thumb/Video128/v4/e9/58/89/e95889a9-deeb-9b41-ed50-0be244289a50/191773963576_1_1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"BTS", "attributes":{"href":"https://itunes.apple.com/us/artist/bts/883131348?uo=2"}}, "title":{"label":"MIC Drop (Steve Aoki Remix) - BTS"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/mic-drop-steve-aoki-remix/1318629400?uo=2"}},{"im:duration":{"label":"29998.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video128/v4/5a/41/75/5a417534-6a95-a4a8-4545-0e5ee4a95262/mzvf_7577338429339611646.640x362.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/mic-drop-steve-aoki-remix/1318629400?uo=2", "attributes":{"im:id":"1318629400"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1686", "term":"K-Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop-k-pop/id1686?uo=2", "label":"K-Pop"}}, "im:releaseDate":{"label":"2017-11-24T00:00:00-07:00", "attributes":{"label":"November 24, 2017"}}},{"im:name":{"label":"Despacito (feat. Daddy Yankee)"}, "rights":{"label":"℗ 2017 Universal Music Latino"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is5.mzstatic.com/image/thumb/Video52/v4/86/bc/b7/86bcb79a-35dd-e254-3b8f-3cba67f1f9f7/vidtrkimg_00602557354324_1_1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is1.mzstatic.com/image/thumb/Video52/v4/86/bc/b7/86bcb79a-35dd-e254-3b8f-3cba67f1f9f7/vidtrkimg_00602557354324_1_1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is1.mzstatic.com/image/thumb/Video52/v4/86/bc/b7/86bcb79a-35dd-e254-3b8f-3cba67f1f9f7/vidtrkimg_00602557354324_1_1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Luis Fonsi", "attributes":{"href":"https://itunes.apple.com/us/artist/luis-fonsi/102834?uo=2"}}, "title":{"label":"Despacito (feat. Daddy Yankee) - Luis Fonsi"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/despacito-feat-daddy-yankee/1194807248?uo=2"}},{"im:duration":{"label":"30000.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video111/v4/3e/07/5c/3e075cc8-b695-bcc0-17eb-250f9b0e6f66/mzvf_765467457299227450.640x472.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/despacito-feat-daddy-yankee/1194807248?uo=2", "attributes":{"im:id":"1194807248"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1612", "term":"Latin", "scheme":"https://itunes.apple.com/us/genre/music-videos-latin/id1612?uo=2", "label":"Latin"}}, "im:releaseDate":{"label":"2017-01-13T00:00:00-07:00", "attributes":{"label":"January 13, 2017"}}},{"im:name":{"label":"While My Guitar Gently Weeps"}, "rights":{"label":"℗ 2012 The Rock And Roll Hall Of Fame Foundation."}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video50/v4/20/ac/93/20ac93f9-8070-3f10-b643-e41d19f851fc/USRYC1290038.sca1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is2.mzstatic.com/image/thumb/Video50/v4/20/ac/93/20ac93f9-8070-3f10-b643-e41d19f851fc/USRYC1290038.sca1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is1.mzstatic.com/image/thumb/Video50/v4/20/ac/93/20ac93f9-8070-3f10-b643-e41d19f851fc/USRYC1290038.sca1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Dhani Harrison, Jeff Lynne, Prince, Steve Winwood & Tom Petty", "attributes":{"href":"https://itunes.apple.com/us/artist/dhani-harrison/187032249?uo=2"}}, "title":{"label":"While My Guitar Gently Weeps - Dhani Harrison, Jeff Lynne, Prince, Steve Winwood & Tom Petty"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/while-my-guitar-gently-weeps-live/1111755322?uo=2"}},{"im:duration":{"label":"30010.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video18/v4/18/d2/09/18d209a6-bc6a-8279-285e-955b8fe5305b/mzvf_3610590851732519339.640x368.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/while-my-guitar-gently-weeps-live/1111755322?uo=2", "attributes":{"im:id":"1111755322"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1621", "term":"Rock", "scheme":"https://itunes.apple.com/us/genre/music-videos-rock/id1621?uo=2", "label":"Rock"}}, "im:releaseDate":{"label":"2012-04-11T00:00:00-07:00", "attributes":{"label":"April 11, 2012"}}},{"im:name":{"label":"Thunder"}, "rights":{"label":"℗ 2017 KIDinaKORNER/Interscope Records"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video52/v4/2f/cd/16/2fcd1670-a401-71d3-4757-d638696ba749/vidtrkimg_00602557630022_1_1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is5.mzstatic.com/image/thumb/Video52/v4/2f/cd/16/2fcd1670-a401-71d3-4757-d638696ba749/vidtrkimg_00602557630022_1_1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is3.mzstatic.com/image/thumb/Video52/v4/2f/cd/16/2fcd1670-a401-71d3-4757-d638696ba749/vidtrkimg_00602557630022_1_1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Imagine Dragons", "attributes":{"href":"https://itunes.apple.com/us/artist/imagine-dragons/358714030?uo=2"}}, "title":{"label":"Thunder - Imagine Dragons"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/thunder/1232085876?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video127/v4/ac/d5/56/acd556bf-b201-c437-6ed9-f37b8d3f03ad/mzvf_8164724897717501581.640x418.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/thunder/1232085876?uo=2", "attributes":{"im:id":"1232085876"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1620", "term":"Alternative", "scheme":"https://itunes.apple.com/us/genre/music-videos-alternative/id1620?uo=2", "label":"Alternative"}}, "im:releaseDate":{"label":"2017-05-02T00:00:00-07:00", "attributes":{"label":"May 2, 2017"}}},{"im:name":{"label":"Meant to Be (feat. Florida Georgia Line)"}, "rights":{"label":"℗ 2017 Warner Bros. Records Inc."}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/f5/12/a7/f512a76f-5e6e-4c2e-1169-7ce4064d3018/USWBV1701455.sca1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is3.mzstatic.com/image/thumb/Video128/v4/f5/12/a7/f512a76f-5e6e-4c2e-1169-7ce4064d3018/USWBV1701455.sca1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/f5/12/a7/f512a76f-5e6e-4c2e-1169-7ce4064d3018/USWBV1701455.sca1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Bebe Rexha", "attributes":{"href":"https://itunes.apple.com/us/artist/bebe-rexha/466059563?uo=2"}}, "title":{"label":"Meant to Be (feat. Florida Georgia Line) - Bebe Rexha"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/meant-to-be-feat-florida-georgia-line/1299445080?uo=2"}},{"im:duration":{"label":"30045.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video128/v4/d9/f1/2e/d9f12e8f-b5cf-92bc-aacf-1cdb9c8b89eb/mzvf_8378291596016316049.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/meant-to-be-feat-florida-georgia-line/1299445080?uo=2", "attributes":{"im:id":"1299445080"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2017-10-23T00:00:00-07:00", "attributes":{"label":"October 23, 2017"}}},{"im:name":{"label":"Marry Me"}, "rights":{"label":"℗ 2017 Big Machine Label Group, LLC"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is2.mzstatic.com/image/thumb/Video128/v4/9c/8e/50/9c8e508b-53be-0dc6-15f4-ba267f86834a/vidtrkimg_00843930034420_1_1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/9c/8e/50/9c8e508b-53be-0dc6-15f4-ba267f86834a/vidtrkimg_00843930034420_1_1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Video128/v4/9c/8e/50/9c8e508b-53be-0dc6-15f4-ba267f86834a/vidtrkimg_00843930034420_1_1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Thomas Rhett", "attributes":{"href":"https://itunes.apple.com/us/artist/thomas-rhett/502541718?uo=2"}}, "title":{"label":"Marry Me - Thomas Rhett"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/marry-me/1326295372?uo=2"}},{"im:duration":{"label":"30045.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video128/v4/ce/7e/89/ce7e8997-a02f-734d-e6f0-6e8a1880c383/mzvf_2188201627879331201.640x326.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/marry-me/1326295372?uo=2", "attributes":{"im:id":"1326295372"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1606", "term":"Country", "scheme":"https://itunes.apple.com/us/genre/music-videos-country/id1606?uo=2", "label":"Country"}}, "im:releaseDate":{"label":"2017-12-18T00:00:00-07:00", "attributes":{"label":"December 18, 2017"}}},{"im:name":{"label":"Yoncé"}, "im:image":[{"label":"http://is3.mzstatic.com/image/thumb/Video6/v4/be/45/8c/be458c8f-422a-b46f-6c93-a7c8ae8a917d/8864443858550121VIC.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is4.mzstatic.com/image/thumb/Video6/v4/be/45/8c/be458c8f-422a-b46f-6c93-a7c8ae8a917d/8864443858550121VIC.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Video6/v4/be/45/8c/be458c8f-422a-b46f-6c93-a7c8ae8a917d/8864443858550121VIC.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:collection":{"im:name":{"label":"BEYONCÉ"}, "link":{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/album/beyonc%C3%A9/780330041?uo=2"}}, "im:contentType":{"im:contentType":{"attributes":{"term":"Album", "label":"Album"}}, "attributes":{"term":"Music", "label":"Music"}}}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "rights":{"label":"℗ (C) 2013 Columbia Records, a Division of Sony Music Entertainment"}, "title":{"label":"Yoncé - Beyoncé"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/yonc%C3%A9/780332973?uo=2"}},{"im:duration":{"label":"30000.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video69/v4/fa/0c/d0/fa0cd059-0d58-ed18-9cf3-407b8f5235df/mzvf_5969335231616428860.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/yonc%C3%A9/780332973?uo=2", "attributes":{"im:id":"780332973"}}, "im:artist":{"label":"Beyoncé", "attributes":{"href":"https://itunes.apple.com/us/artist/beyonc%C3%A9/1419227?uo=2"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2013-12-13T00:00:00-07:00", "attributes":{"label":"December 13, 2013"}}},{"im:name":{"label":"End Game (feat. Ed Sheeran & Future)"}, "rights":{"label":"℗ 2018 Big Machine Label Group, LLC"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video118/v4/56/6c/7e/566c7ec9-8a80-eb5e-c071-42aec176aab3/vidtrkimg_00843930034550_1_1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is2.mzstatic.com/image/thumb/Video118/v4/56/6c/7e/566c7ec9-8a80-eb5e-c071-42aec176aab3/vidtrkimg_00843930034550_1_1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Video118/v4/56/6c/7e/566c7ec9-8a80-eb5e-c071-42aec176aab3/vidtrkimg_00843930034550_1_1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Taylor Swift", "attributes":{"href":"https://itunes.apple.com/us/artist/taylor-swift/159260351?uo=2"}}, "title":{"label":"End Game (feat. Ed Sheeran & Future) - Taylor Swift"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/end-game-feat-ed-sheeran-future/1333442764?uo=2"}},{"im:duration":{"label":"30045.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video128/v4/eb/27/67/eb27679b-f373-57ce-7776-d585a67f2fed/mzvf_2062762106238987942.640x410.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/end-game-feat-ed-sheeran-future/1333442764?uo=2", "attributes":{"im:id":"1333442764"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2018-01-12T00:00:00-07:00", "attributes":{"label":"January 12, 2018"}}},{"im:name":{"label":"For You (Fifty Shades Freed)"}, "rights":{"label":"℗ 2018 Universal Studios Capitol Records UK & Atlantic Records UK"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is3.mzstatic.com/image/thumb/Video128/v4/59/5d/e5/595de5a8-b491-21cd-4e96-ac3508459e1a/vidtrkimg_00602567436461_1_1.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is1.mzstatic.com/image/thumb/Video128/v4/59/5d/e5/595de5a8-b491-21cd-4e96-ac3508459e1a/vidtrkimg_00602567436461_1_1.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is3.mzstatic.com/image/thumb/Video128/v4/59/5d/e5/595de5a8-b491-21cd-4e96-ac3508459e1a/vidtrkimg_00602567436461_1_1.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Liam Payne & Rita Ora", "attributes":{"href":"https://itunes.apple.com/us/artist/liam-payne/366710817?uo=2"}}, "title":{"label":"For You (Fifty Shades Freed) - Liam Payne & Rita Ora"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/for-you-fifty-shades-freed/1341173911?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video118/v4/26/17/67/26176737-7b8e-c272-d459-138f508b7acf/mzvf_9020349673464753395.640x334.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/for-you-fifty-shades-freed/1341173911?uo=2", "attributes":{"im:id":"1341173911"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1616", "term":"Soundtrack", "scheme":"https://itunes.apple.com/us/genre/music-videos-soundtrack/id1616?uo=2", "label":"Soundtrack"}}, "im:releaseDate":{"label":"2018-01-26T00:00:00-07:00", "attributes":{"label":"January 26, 2018"}}},{"im:name":{"label":"Cupid Shuffle"}, "im:image":[{"label":"http://is1.mzstatic.com/image/thumb/Music/90/e5/f4/mzi.qxcrzcib.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is2.mzstatic.com/image/thumb/Music/90/e5/f4/mzi.qxcrzcib.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Music/90/e5/f4/mzi.qxcrzcib.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:collection":{"im:name":{"label":"Cupid Shuffle - EP"}, "link":{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/album/cupid-shuffle-ep/260167050?uo=2"}}, "im:contentType":{"im:contentType":{"attributes":{"term":"Album", "label":"Album"}}, "attributes":{"term":"Music", "label":"Music"}}}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "rights":{"label":"℗ 2007 Atlantic Recording Corporation for the United States and WEA International Inc. for the world outside of the United States"}, "title":{"label":"Cupid Shuffle - Cupid"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/cupid-shuffle/260167057?uo=2"}},{"im:duration":{"label":"231981.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video/15/37/83/mzm.urgojmfi..640x352.h264lc.u.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/cupid-shuffle/260167057?uo=2", "attributes":{"im:id":"260167057"}}, "im:artist":{"label":"Cupid", "attributes":{"href":"https://itunes.apple.com/us/artist/cupid/25268009?uo=2"}}, "category":{"attributes":{"im:id":"1615", "term":"R&B/Soul", "scheme":"https://itunes.apple.com/us/genre/music-videos-r-b-soul/id1615?uo=2", "label":"R&B/Soul"}}, "im:releaseDate":{"label":"2007-07-31T00:00:00-07:00", "attributes":{"label":"July 31, 2007"}}},{"im:name":{"label":"Man of the Woods"}, "rights":{"label":"℗ (C) 2018 RCA Records, a division of Sony Music Entertainment"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video128/v4/8f/e1/09/8fe10985-0c40-63d7-79c8-2783d0d97ac5/8864469722440101.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/8f/e1/09/8fe10985-0c40-63d7-79c8-2783d0d97ac5/8864469722440101.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is3.mzstatic.com/image/thumb/Video128/v4/8f/e1/09/8fe10985-0c40-63d7-79c8-2783d0d97ac5/8864469722440101.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Justin Timberlake", "attributes":{"href":"https://itunes.apple.com/us/artist/justin-timberlake/398128?uo=2"}}, "title":{"label":"Man of the Woods - Justin Timberlake"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/man-of-the-woods-official-video/1343361774?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video118/v4/2d/08/95/2d089539-9538-a200-3e3d-8602e01d3ba0/mzvf_2734005804724784746.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/man-of-the-woods-official-video/1343361774?uo=2", "attributes":{"im:id":"1343361774"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2018-02-03T00:00:00-07:00", "attributes":{"label":"February 3, 2018"}}},{"im:name":{"label":"Uptown Funk (feat. Bruno Mars)"}, "rights":{"label":"℗ (C) 2014 Mark Ronson under exclusive licence to Sony Music Entertainment UK Limited"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video1/v4/59/a2/97/59a297fa-11f3-459c-867c-25e71fa9f55e/8864449528660101VIC.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is2.mzstatic.com/image/thumb/Video1/v4/59/a2/97/59a297fa-11f3-459c-867c-25e71fa9f55e/8864449528660101VIC.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is2.mzstatic.com/image/thumb/Video1/v4/59/a2/97/59a297fa-11f3-459c-867c-25e71fa9f55e/8864449528660101VIC.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Mark Ronson", "attributes":{"href":"https://itunes.apple.com/us/artist/mark-ronson/1806833?uo=2"}}, "title":{"label":"Uptown Funk (feat. Bruno Mars) - Mark Ronson"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/uptown-funk-feat-bruno-mars/942813466?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video122/v4/08/29/52/0829529e-bc13-8973-f2b8-a06c13458db5/mzvf_5859322578387528157.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/uptown-funk-feat-bruno-mars/942813466?uo=2", "attributes":{"im:id":"942813466"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2014-11-20T00:00:00-07:00", "attributes":{"label":"November 20, 2014"}}},{"im:name":{"label":"Can't Stop the Feeling! (From DreamWorks Animation's \"Trolls\")"}, "rights":{"label":"℗ (C) 2016 RCA Records/DreamWorks Animation LLC"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is2.mzstatic.com/image/thumb/Video50/v4/35/d5/ee/35d5eef1-bf59-7ed4-a26c-a142671d272a/8864459030410101VIC.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is3.mzstatic.com/image/thumb/Video50/v4/35/d5/ee/35d5eef1-bf59-7ed4-a26c-a142671d272a/8864459030410101VIC.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is1.mzstatic.com/image/thumb/Video50/v4/35/d5/ee/35d5eef1-bf59-7ed4-a26c-a142671d272a/8864459030410101VIC.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Justin Timberlake", "attributes":{"href":"https://itunes.apple.com/us/artist/justin-timberlake/398128?uo=2"}}, "title":{"label":"Can't Stop the Feeling! (From DreamWorks Animation's \"Trolls\") - Justin Timberlake"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/cant-stop-the-feeling-from-dreamworks-animations-trolls/1114634523?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video118/v4/5d/d6/f3/5dd6f323-6b31-3d77-99ff-4dce85633300/mzvf_8271124529726310650.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/cant-stop-the-feeling-from-dreamworks-animations-trolls/1114634523?uo=2", "attributes":{"im:id":"1114634523"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2016-05-18T00:00:00-07:00", "attributes":{"label":"May 18, 2016"}}},{"im:name":{"label":"Filthy"}, "rights":{"label":"℗ (C) 2018 RCA Records, a division of Sony Music Entertainment"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is5.mzstatic.com/image/thumb/Video118/v4/65/bc/14/65bc14f9-b5f5-1063-d875-a44e5d743c42/8864469288760101VIC.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is3.mzstatic.com/image/thumb/Video118/v4/65/bc/14/65bc14f9-b5f5-1063-d875-a44e5d743c42/8864469288760101VIC.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is3.mzstatic.com/image/thumb/Video118/v4/65/bc/14/65bc14f9-b5f5-1063-d875-a44e5d743c42/8864469288760101VIC.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Justin Timberlake", "attributes":{"href":"https://itunes.apple.com/us/artist/justin-timberlake/398128?uo=2"}}, "title":{"label":"Filthy - Justin Timberlake"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/filthy/1331245045?uo=2"}},{"im:duration":{"label":"29998.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video118/v4/7a/1e/cc/7a1ecc7f-16eb-cc1f-9576-3a969346257b/mzvf_4445579200821031487.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/filthy/1331245045?uo=2", "attributes":{"im:id":"1331245045"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1614", "term":"Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop/id1614?uo=2", "label":"Pop"}}, "im:releaseDate":{"label":"2018-01-12T00:00:00-07:00", "attributes":{"label":"January 12, 2018"}}},{"im:name":{"label":"Bad Boy"}, "rights":{"label":"℗ 2018 SM Entertainment"}, "im:price":{"label":"$1.99", "attributes":{"amount":"1.99000", "currency":"USD"}}, "im:image":[{"label":"http://is4.mzstatic.com/image/thumb/Video128/v4/82/7b/9e/827b9ee8-acd4-31f7-ea2b-c85674907d08/KRZ261800003.jpg/71x53bb-85.jpg", "attributes":{"height":"53"}},{"label":"http://is5.mzstatic.com/image/thumb/Video128/v4/82/7b/9e/827b9ee8-acd4-31f7-ea2b-c85674907d08/KRZ261800003.jpg/80x60bb-85.jpg", "attributes":{"height":"60"}},{"label":"http://is4.mzstatic.com/image/thumb/Video128/v4/82/7b/9e/827b9ee8-acd4-31f7-ea2b-c85674907d08/KRZ261800003.jpg/100x100bb-85.jpg", "attributes":{"height":"100"}}], "im:artist":{"label":"Red Velvet", "attributes":{"href":"https://itunes.apple.com/us/artist/red-velvet/906961899?uo=2"}}, "title":{"label":"Bad Boy - Red Velvet"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/us/music-video/bad-boy/1342113649?uo=2"}},{"im:duration":{"label":"30022.0"}, "attributes":{"title":"Preview", "rel":"enclosure", "type":"video/x-m4v", "href":"http://video.itunes.apple.com/apple-assets-us-std-000001/Video62/v4/82/bd/37/82bd3747-f84c-c8ba-6a9e-010f51689cef/mzvf_4264131740120245108.640x480.h264lc.U.p.m4v", "im:assetType":"preview"}}], "id":{"label":"https://itunes.apple.com/us/music-video/bad-boy/1342113649?uo=2", "attributes":{"im:id":"1342113649"}}, "im:contentType":{"attributes":{"term":"Music Video", "label":"Music Video"}}, "category":{"attributes":{"im:id":"1686", "term":"K-Pop", "scheme":"https://itunes.apple.com/us/genre/music-videos-pop-k-pop/id1686?uo=2", "label":"K-Pop"}}, "im:releaseDate":{"label":"2018-01-29T00:00:00-07:00", "attributes":{"label":"January 29, 2018"}}}], "updated":{"label":"2018-02-09T13:49:52-07:00"}, "rights":{"label":"Copyright 2008 Apple Inc."}, "title":{"label":"iTunes Store: Top Music Videos"}, "icon":{"label":"http://itunes.apple.com/favicon.ico"}, "link":[{"attributes":{"rel":"alternate", "type":"text/html", "href":"https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewTop?cc=us&id=1&popId=5"}},{"attributes":{"rel":"self", "href":"https://itunes.apple.com/us/rss/topmusicvideos/limit=20/json"}}], "id":{"label":"https://itunes.apple.com/us/rss/topmusicvideos/limit=20/json"}}};export default data;
Now that you have accurate data, you'll use it by the provider, in the next lab step.
Q: What's the best way to write a difficult piece of code?
A: Assign it
to the summer intern. ;-)
Here's the code the intern came up with. Edit providers/itunes/itunes.ts and replace it with this code.
import { Injectable } from "@angular/core";import testData from "./test-data";@Injectable()export class ItunesProvider {constructor() {console.log("Hello ItunesProvider Provider");}get() {return new Promise<{}[]>((resolve, reject) => {var result = testData.feed.entry.map(item => {return {artist: item["im:artist"].label,title: item.title.label,thumbnail: item["im:image"][2].label,video: item.link[1].attributes.href,store: item.link[0].attributes.href};});resolve(result);});}}
The array of iTunes data is nested in the feed.entry
property. The code uses the array prototype's map
function to transform each value in the array into the
simpler value expected by the view.
Save your changes and you everything should work. You should see 20 music videos.
Now that the data is more complete, and you have a tumbnail image, etc., you can make the list a little nicer.
Edit pages/home/home.html and replace it with this.
<ion-header><ion-navbar><ion-title><ion-title>Top {{tunes.length}} iTunes Music Videos</ion-title></ion-title></ion-navbar></ion-header><ion-content padding><ion-list><ion-item *ngFor="let tune of tunes" (click)="onItemClick(tune)"><ion-thumbnail item-start><img src="{{tune.thumbnail}}" /></ion-thumbnail><h1>{{tune.title}}</h1><h2>{{tune.artist}}</h2></ion-item></ion-list></ion-content>
This adds an <ion-thumbnail>, and makes the title and artist name a little
more prominant.
Save and look at the result. It's nicer, but some images are stretched a little strangely. iTunes doesn't use consistent image sizes, so we need to tweak those a little.
To do that, edit pages/home/home.scss and use this code.
page-home {ion-item {ion-thumbnail {position: relative;img {width: auto !important;height: auto !important;position: absolute;top: 50%;transform: translate(0, -50%);}}}}
This scales the images more nicely, and centers them vertically.
Save and refresh, and the list looks even nicer.

Note that at this point you could actually finish the preview page and show the video. That's because the mocked up data is accurate, and holds those values. It's often a good style of coding to accurately mock out certain aspects of the application. In this case, the view logic doesn't actually care whether the data is up-to-date. Furthermore, in a team setting, the people coding the view could finish up their work even while the people coding the data access are still ironing out details.
// Normal functions have a "this" variable. Arrow functions
// do not. If you use "this", it will reference the value from
// closure scope. Arrow functions don't have "arguments" either.
function foo(){
setTimeout(() => console.log(`logs ${this.bar} after .1 seconds.`), 100);
console.log(`logs ${this.bar} immediately.`);
}
var o = {bar:'moose', fn:foo};
o.fn()
console.log( beatles.map( (beatle) => {
return beatle.first;
} )
);
// If only one parameter is passed, you can omit the parentheses.
console.log( beatles.map( beatle => {
return beatle.first
} );
const beatles = [ { "first":'John' ,"last":"Lenon" },
{ "first":'Paul' ,"last":'McCartney' },
{ "first":'George' ,"last":'Harrison' },
{ "first":'Ringo' ,"last":'Starr' } ];
// If the return expression returns an object literal, you must
// surround it with parentheses or the parser gets confused. Of
// course *any* expression may be surrounded by parentheses.
// 1 + 1 === (1 + 1)
console.log( beatles.map( beatle =>
( {"full":`${beatle.first} ${beatle.last}`} )
)
);
// If the return value is a single expression, you can use
// the expression alone and omit the "return"
console.log( beatles.map( beatle => beatle.first) ); function foo() {
console.log('before'); // Logs immediately
resolveAfter(1000).then(result => {
console.log(result); // Logs after 1 second
resolveAfter(3000).then(result => {
console.log(result); // Logs 3 seconds later
});
});
}
foo();async function bar() {
console.log('before'); // Logs immediately
console.log( await resolveAfter(1000) ); // Logs after 1 second
console.log( await resolveAfter(3000) ); // Logs 3 seconds later
}
bar();
function resolveAfter(s) {
return new Promise(resolve =>
setTimeout(() => resolve(`resolved after ${s}`), s)
);
}
In this lab the provider's API is established, and you're using it in the view. But the provider is returning accurate-but-mocked data. In this lab you'll actually fetch data from iTunes.
Now that you've gotten the code to work with accurate data, you feel confident about fetching data from iTunes.
For that, you'll use the axios HTTP package.
Use a terminal window to navigate to your project folder, then run this command.
npm install axiosThis installs the package, and updates project dependencies.
Then edit providers/itunes/itunes.ts.
You need to do two things: Import axios, and modify the
get method to use it. Look at the following
code and inspect the get method. Then simply
replace providers/itunes/itunes.ts completely with
the code.
import { Injectable } from "@angular/core";
import axios from "axios";
import testData from "./test-data";
@Injectable()
export class ItunesProvider {
constructor() {
console.log("Hello ItunesProvider Provider");
}
get() {
let url = "https://itunes.apple.com/us/rss/topmusicvideos/limit=50/json";
return axios.get(url).then(function(response) {
var result = response.data.feed.entry.map(item => {
return {
artist: item["im:artist"].label,
title: item["im:name"].label,
thumbnail: item["im:image"][2].label,
video: item.link[1].attributes.href,
store: item.link[0].attributes.href
};
});
return result;
});
}
}The app should work, and you can verify the results in a couple of ways.
First, you should be seeing 50 music videos (rather than the 20 that are in the test data). And in the Chrome debugger you should see network traffic to iTunes.
get methodYou may have noticed a couple of awkward things in the
get method.
First, a one-line arrow function that returns something can be written more succinctly.
Here's a cleaner version.
get() {
const url = "https://itunes.apple.com/us/rss/topmusicvideos/limit=50/json";
return axios.get(url).then(function(response) {
var result = response.data.feed.entry.map(item => ({
artist: item["im:artist"].label,
title: item.title.label,
thumbnail: item["im:image"][2].label,
video: item.link[1].attributes.href,
store: item.link[0].attributes.href
}));
return result;
});
}Note how the expression — which is the object being mapped — is now embedded in parentheses. Any Javascript expression can be embedded in paremtheses, but in this case you had to, so the Javascript parser didn't see the opening bracket and think it was the start of a function body.
Copy and paste that version of get into your
provider, and make sure everything works.
ES2017 introduced async and await syntax to make
using promises easier. Check out this version of get.
async get() {
const url = "https://itunes.apple.com/us/rss/topmusicvideos/limit=50/json";
const response = await axios.get(url);
return response.data.feed.entry.map(item => ({
artist: item["im:artist"].label,
title: item.title.label,
thumbnail: item["im:image"][2].label,
video: item.link[1].attributes.href,
store: item.link[0].attributes.href
}));
}Good code is succinct, clear and easy to maintain. And when it isn't clear, it's well documented. In how many of those ways has this code been improved?
Don't answer that. ;-)
class DebugNode {
constructor(nativeNode: any, parent: DebugNode | null, _debugContext: DebugContext)
nativeNode: any
listeners: EventListener[]
parent: DebugElement | null
get injector: Injector
get componentInstance: any
get context: any
get references: {...}
get providerTokens: any[]
}ng.probe(document.querySelector('page-home')).componentInstance
ng.probe($0).componentInstance
{
artist: item["im:artist"].label,
title: item.title.label,
thumbnail: item["im:image"][2].label,
video: item.link[1].attributes.href,
store: item.link[0].attributes.href
}
tunes: {
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
}[];
tunes:Tune[];
// import this where needed
interface Tune: {
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
}
In this lab you'll refactor the code to type the data.
As a first step, you'll simply type the data inline. There are few places where the structure of the iTunes entry can be specified:
get methodget method, where the items are being mappedThe data structure is
{
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
}[]You need to use that to type the properties holding the data, or in the functions returning the data.
First, edit providers/itunes/itunes.ts and specify
that the get method returns a specific data structure:
async get(): Promise<
{
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
}[]
>
...Be careful about copying and pasting, since you may not
have used async for your implementation of get.
Then edit pages/home/home.ts and replace the class's tunes property with this code.
tunes: {
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
}[] = [];Save your changes, and everything should still work.
Edit pages/home/home.ts and change the type of the
store to a number.
tunes: {
artist: string;
title: string;
thumbnail: string;
video: string;
store: number;
}[] = [];You should get a linting error. That's nice — at least the linter is telling you did something inconsistently.
But it would better to describe that data structure as an interface, then use the interface type. In that way, you won't have the opportunity to be inconsistent!
You'll do that in the following steps.
Before moving on, change the store property back to string
and verify that your code still works.
Rather than repeatedly describe the data structure throughout your app, it's better to use an interface. By doing so, you have a single point of maintenance.
Create the file providers/itunes/tune.ts with this content.
export interface Tune {
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
}Now you have to replace where the data is being stored and replaced.
First, edit providers/itunes/itunes.ts and import
the new interface.
import { Tune } from "./tune";Now replace the get return type.
async get(): Promise<Tune[]> {
...
}Save your changes and everything should still work.
Now edit pages/home/home.ts and do two things.
Import the interface type.
import { Tune } from "../../providers/itunes/tune";Then change the class property to this.
tunes: Tune[] = [];Save your changes and everything should still work.
These changes result in the iTunes data structure being formally typed. That means it's less likely for code changes to accidentally introduce a bug.
const simpsons = [ { name: 'Homer' , age: 36 },
{ name: 'Marge' , age: 34 },
{ name: 'Bart' , age: 10 },
{ name: 'Lisa' , age: 7 },
{ name: 'Maggie' , age: 1 } ];
// Default runs toString on the objects, so they
// all evaluate to [object Object] -- no effect
simpsons() .sort();
// Sorted by name using built-in localeCompare method.
simpsons .sort( (a, b) => a.name.localeCompare(b.name) );
// Sorted by age via returning the numeric difference.
simpsons .sort( (a, b) => (a.age - b.age) );In this lab you'll sort the data.
You're getting data in the order provied by iTunes. Perhaps it's in order of sales, or ordered by release date. But our users want the data sorted by title within artist.
The JavaScript array type has a sort function, that takes a comparator as its parameter.
Edit providers/itunes/itunes.ts and change the get method.
async get(): Promise<Tune[]> {
const url = "https://itunes.apple.com/us/rss/topmusicvideos/limit=50/json";
const response = await axios.get(url);
const result = response.data.feed.entry.map(item => ({
artist: item["im:artist"].label,
title: item.title.label,
thumbnail: item["im:image"][2].label,
video: item.link[1].attributes.href,
store: item.link[0].attributes.href
}));
result.sort((a, b) => a.artist.localeCompare(b.artist));
return result;
}Save and confirm that the data is sorted by artists' names.

You're seeing the tunes sorted by artist. But that's not good enough! We want the data sorted by title within artist.
Modify the sort comparator. When the artist names are the same, you need to return the title comparison.
See if you can figure that out.
Did you get the sort working? Here's one way you could do it (written in nearly-invisible ink.)
result.sort( (a, b) => a.artist.localeCompare(b.artist) || a.title.localeCompare(b.title) );
Remember that an arrow function with a single statement implicitly returns the value.
Here, the statement
uses the || short-circuit or. This means that if the
left operant is truthy, it uses it because there's
no need to avaluate the right-hand side. If the left-hand
operand is falsy, then it uses the value for the right
operand.
In this case, if the artist name differs, and a.artist.localeCompare(b.artist) returns a non-zero value,
we want to use it. But it it's the same artist name —
and it's 0 or falsy — then we want to use the
right operand, or the song title comparison.
<ion-list>
<ion-item *ngFor="let tune of tunes" (click)="onItemClick(tune)">
<ion-thumbnail item-start>
<img src="{{tune.thumbnail}}" />
</ion-thumbnail>
<h1>{{tune.title}}</h1>
<h2>{{tune.artist}}</h2>
</ion-item>
</ion-list>
<ion-list>
<list-item *ngFor="let tune of tunes" (click)="onItemClick(tune)">
</list-item>
</ion-list>
ionic generate component
ionic g component
ionic generate ? What would you like to generate: (Use arrow keys) > component directive page pipe provider tabs
In this lab you'll refactor the item detail into its own component.
The home view loops through each tune, and shows details in a list. But the list items are getting a little complicated. The code will be more modular and easier to maintain if the item is in its own class.
First, use the cli to create a new page. Use a terminal window to navigate to your project folder, then run this command.
ionic generate component TuneItemIonic will generate a new component, and an associated module
named ComponentsModule. The
module is set up to hold any future component.
If you look at your project, you'll see a new directory
named components that holds the module, and the new
ListItem
src
components
tune-item
tune-item.html
tune-item.scss
tune-item.ts
components.module.tsThen edit src/app/app.module.ts and do two things:
ComponentsModule
ComponentsModule to the module's imports arrayNote that you're importing the new module, not the new component specifically. The reason is that if you add new components later, you maintain the component module, rather than the app module. Since the app module imports the component module, changes to the component module will be included in the app.
Then open pages/home/home.html and look at the <ion-item>...</ion-item>. This is the block that needs to be
in the new list item.
You could copy and paste from home.html, but to save you the effort,
simply replace the contents of components/tune-item/list-item.html with this code.
<ion-item>
<ion-thumbnail item-start>
<img src="{{tune.thumbnail}}" />
</ion-thumbnail>
<h1>{{tune.title}}</h1>
<h2>{{tune.artist}}</h2>
</ion-item>After you save tune-item.html the linter will give you errors, stating: 'ion-item' is not a known element.
This is because IonicModule is what brings in the Ionic components, and the
new components module isn't importing that.
Edit components/components-module.ts and replace its contents with this.
import { NgModule } from "@angular/core";
import { IonicModule } from "ionic-angular";
import { TuneItemComponent } from "./tune-item/tune-item";
@NgModule({
declarations: [TuneItemComponent],
imports: [IonicModule],
exports: [TuneItemComponent]
})
export class ComponentsModule {}
This code imports IonicModule.
The linting error in tune-item.html should go away. (You may need to close and reopen the editor.)
Now edit pages/home/home.html and replace the <ion-item>...</ion-item>
with this.
<tune-item [tune]="tune" (click)="onItemClick(tune)" *ngFor="let tune of tunes"></tune-item>But now you hit another linting error, for the [tune] attribute. The
new list item class doesn't have that property yet.
You need to edit components/tune-item/tune-item.ts to add a tune property.
But you need to do it in such a way that the value can be passed.
Angular lets you pass data from parent to child with input
binding. To do that, simply decorate the tune property with the @Input()
decorator. You also need to import Input, as well as Tune.
Use this code for components/list-item/list-item.ts.
import { Component, Input } from "@angular/core";
import { Tune } from "../../providers/itunes/tune";
@Component({
selector: "tune-item",
templateUrl: "tune-item.html"
})
export class TuneItemComponent {
@Input() tune: Tune;
constructor() {
console.log("Hello ListItemComponent Component");
}
}The code also includes a console.log in the constructor, to make it easier
to see when the component is being used.
After saving this change, you won't have any linting errors in tune-item.html
any more.
Save and refresh and you'll see the list item's console.log being run
50 times.

<video src="[url]" poster="[url]" controls autoplay loop > Sorry, your browser doesn't support embedded videos. :-( </video>
controls — If this attribute is present, the user sees controls such as play, pause and volume.
autoplay — If this attribute is present, the video plays immediately. This is generally discouraged.
loop — If this attribute is present, the video starts playing again at the end of the vide.
src — The URL of the video.
poster — The URL of a thumbnail image.
In this lab you'll use a <video> tag on the preview page.
Edit pages/preview/preview.html and use this code.
<ion-header>
<ion-navbar>
<h3>{{tune.title}} — {{tune.artist}}</h3>
</ion-navbar>
</ion-header>
<ion-content padding>
<video autoplay>
<source src="{{tune.video}}">
</video>
</ion-content>Note the video autoplay attribute — that means the
video will play immediately. Notmally that's discouraged.
It's better to let the user manually start videos. But the
whole purpose of the detail page is to show the video, so
in this case using autoplay is ok.
Save and try things out. Now you can choose and item and see the music video!
Apple wants to make it very easy for people to buy things on the app store, and one of the ways they do that is by requiring that we provide a get it on iTunes link.
To do that, you need an image, and a button that holds the link.
Create the file src/assets/imgs/get-it-on-itunes.svg with
this content.
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Livetype" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="110px" height="40px" viewBox="0 0 110 40" enable-background="new 0 0 110 40" xml:space="preserve">
<g>
<path fill="#A6A6A6" d="M103.371,0H6.625C6.372,0,6.119,0,5.866,0.001C5.652,0.002,5.443,0.005,5.23,0.01
c-0.465,0.016-0.934,0.04-1.394,0.125c-0.467,0.082-0.9,0.22-1.32,0.436C2.098,0.783,1.72,1.057,1.385,1.386
C1.056,1.721,0.783,2.098,0.57,2.517C0.355,2.936,0.217,3.37,0.135,3.837c-0.087,0.46-0.11,0.929-0.126,1.394
C0.004,5.444,0.002,5.652,0.001,5.866C0,6.119,0,6.372,0,6.625v26.753c0,0.248,0,0.506,0.001,0.756
c0.001,0.211,0.003,0.426,0.008,0.639c0.016,0.471,0.04,0.934,0.126,1.389c0.082,0.473,0.22,0.906,0.435,1.33
c0.212,0.408,0.485,0.799,0.815,1.121c0.335,0.334,0.712,0.613,1.131,0.824c0.419,0.217,0.853,0.344,1.32,0.438
c0.46,0.08,0.929,0.105,1.394,0.115c0.213,0.004,0.422,0.006,0.636,0.008C6.119,40,6.372,40,6.625,40h96.747
c0.252,0,0.506,0,0.76-0.002c0.211-0.002,0.426-0.004,0.641-0.008c0.463-0.01,0.932-0.035,1.393-0.115
c0.461-0.094,0.895-0.221,1.32-0.438c0.42-0.211,0.797-0.49,1.127-0.824c0.328-0.322,0.607-0.713,0.818-1.121
c0.217-0.424,0.354-0.857,0.436-1.33c0.082-0.455,0.111-0.918,0.123-1.389c0.008-0.213,0.01-0.428,0.01-0.639
C110,33.885,110,33.627,110,33.379V6.625c0-0.254,0-0.506-0.002-0.76c0-0.213-0.002-0.421-0.01-0.635
c-0.012-0.465-0.041-0.934-0.123-1.394c-0.082-0.467-0.219-0.901-0.436-1.32c-0.211-0.419-0.49-0.796-0.818-1.131
c-0.33-0.329-0.707-0.603-1.127-0.815c-0.426-0.215-0.859-0.354-1.32-0.436c-0.461-0.086-0.93-0.11-1.393-0.125
c-0.215-0.005-0.43-0.007-0.641-0.008C103.877,0,103.623,0,103.371,0L103.371,0z"/>
<path d="M103.371,0.985l0.752,0.001c0.207,0.001,0.416,0.003,0.619,0.013c0.379,0.009,0.818,0.027,1.246,0.108
c0.395,0.071,0.727,0.18,1.051,0.342c0.32,0.166,0.615,0.38,0.875,0.638c0.258,0.258,0.473,0.552,0.639,0.876
c0.162,0.322,0.271,0.654,0.342,1.05c0.076,0.423,0.096,0.865,0.105,1.24c0.006,0.207,0.008,0.415,0.008,0.625
c0.002,0.25,0.002,0.496,0.002,0.748v26.753c0,0.246,0,0.502-0.002,0.75c0,0.207-0.002,0.416-0.008,0.621
c-0.01,0.377-0.029,0.818-0.105,1.244c-0.07,0.387-0.18,0.725-0.342,1.053c-0.166,0.318-0.381,0.613-0.639,0.875
c-0.26,0.254-0.555,0.467-0.877,0.627c-0.322,0.174-0.654,0.277-1.047,0.35c-0.434,0.074-0.893,0.104-1.244,0.111
c-0.205,0.004-0.412,0.006-0.623,0.008c-0.25,0.002-0.502,0.002-0.752,0.002H6.625c-0.002,0-0.005,0-0.007,0
c-0.249,0-0.497,0-0.748-0.002c-0.203-0.002-0.412-0.004-0.617-0.008c-0.349-0.008-0.81-0.037-1.241-0.111
c-0.395-0.072-0.727-0.176-1.051-0.35c-0.323-0.16-0.617-0.373-0.875-0.627c-0.257-0.262-0.471-0.557-0.638-0.875
c-0.161-0.328-0.271-0.666-0.341-1.055c-0.082-0.426-0.099-0.865-0.108-1.242c-0.01-0.207-0.012-0.416-0.013-0.621l0-0.6v-0.15
V6.625v-0.15l0-0.598c0.001-0.208,0.003-0.416,0.013-0.624c0.009-0.374,0.026-0.814,0.108-1.241c0.07-0.394,0.18-0.727,0.341-1.05
C1.615,2.639,1.83,2.345,2.087,2.087c0.257-0.257,0.551-0.472,0.875-0.639c0.323-0.161,0.655-0.27,1.05-0.341
C4.438,1.026,4.878,1.008,5.253,1C5.46,0.99,5.668,0.988,5.876,0.987l0.749-0.001H103.371"/>
<g>
<g>
<path fill="#FFFFFF" d="M30.128,19.784c-0.029-3.223,2.639-4.791,2.761-4.864c-1.511-2.203-3.853-2.504-4.676-2.528
c-1.967-0.207-3.875,1.177-4.877,1.177c-1.022,0-2.565-1.157-4.228-1.123c-2.14,0.033-4.142,1.272-5.24,3.196
c-2.266,3.923-0.576,9.688,1.595,12.858c1.086,1.553,2.355,3.287,4.016,3.227c1.625-0.068,2.232-1.037,4.193-1.037
c1.943,0,2.513,1.037,4.207,0.998c1.744-0.029,2.842-1.561,3.89-3.127c1.255-1.781,1.759-3.533,1.779-3.623
C33.507,24.924,30.161,23.646,30.128,19.784z"/>
<path fill="#FFFFFF" d="M26.928,10.306c0.874-1.093,1.472-2.58,1.306-4.089c-1.265,0.056-2.847,0.875-3.758,1.944
c-0.806,0.942-1.526,2.486-1.34,3.938C24.557,12.205,26.016,11.382,26.928,10.306z"/>
</g>
</g>
<g>
<path fill="#FFFFFF" d="M49.479,13.092c-0.609,0.23-1.252,0.345-1.929,0.345c-1.006,0-1.792-0.273-2.358-0.82
c-0.584-0.565-0.876-1.342-0.876-2.33c0-0.981,0.312-1.771,0.937-2.367c0.625-0.597,1.446-0.895,2.465-0.895
c0.652,0,1.177,0.096,1.575,0.289l-0.224,0.82c-0.386-0.174-0.842-0.261-1.37-0.261c-0.702,0-1.262,0.202-1.678,0.606
c-0.429,0.423-0.643,1.007-0.643,1.752s0.205,1.333,0.615,1.761c0.391,0.41,0.926,0.615,1.603,0.615
c0.422,0,0.727-0.043,0.913-0.13v-1.706h-1.146V9.979h2.115V13.092z"/>
<path fill="#FFFFFF" d="M54.727,10.893c0,0.18-0.013,0.333-0.037,0.457h-3.02c0.013,0.447,0.159,0.789,0.438,1.025
c0.255,0.211,0.584,0.317,0.988,0.317c0.447,0,0.854-0.072,1.221-0.214l0.159,0.699c-0.429,0.187-0.936,0.28-1.52,0.28
c-0.702,0-1.253-0.207-1.654-0.62c-0.4-0.413-0.601-0.967-0.601-1.664c0-0.683,0.187-1.252,0.559-1.706
c0.392-0.484,0.919-0.727,1.584-0.727c0.652,0,1.146,0.242,1.482,0.727C54.593,9.853,54.727,10.328,54.727,10.893z M53.767,10.632
c0.006-0.298-0.059-0.556-0.196-0.773c-0.174-0.28-0.441-0.419-0.801-0.419c-0.33,0-0.597,0.137-0.802,0.41
c-0.168,0.218-0.267,0.479-0.298,0.783H53.767z"/>
<path fill="#FFFFFF" d="M58.324,9.598h-1.109v2.199c0,0.559,0.196,0.839,0.588,0.839c0.18,0,0.329-0.016,0.447-0.046l0.027,0.764
c-0.198,0.075-0.459,0.112-0.782,0.112c-0.397,0-0.708-0.121-0.932-0.364c-0.224-0.242-0.335-0.649-0.335-1.221V9.598h-0.662
V8.843h0.662v-0.83l0.987-0.298v1.128h1.109V9.598z"/>
<path fill="#FFFFFF" d="M63.264,7.491c0,0.18-0.06,0.326-0.178,0.438c-0.117,0.112-0.27,0.168-0.457,0.168
c-0.167,0-0.309-0.058-0.424-0.173c-0.115-0.115-0.172-0.259-0.172-0.433s0.059-0.317,0.178-0.429
c0.117-0.112,0.264-0.168,0.438-0.168s0.32,0.056,0.438,0.168C63.204,7.174,63.264,7.317,63.264,7.491z M63.151,13.372h-1.007
V8.843h1.007V13.372z"/>
<path fill="#FFFFFF" d="M67.074,9.598h-1.108v2.199c0,0.559,0.196,0.839,0.587,0.839c0.18,0,0.33-0.016,0.447-0.046l0.027,0.764
c-0.197,0.075-0.459,0.112-0.781,0.112c-0.398,0-0.709-0.121-0.933-0.364c-0.224-0.242-0.335-0.649-0.335-1.221V9.598h-0.662
V8.843h0.662v-0.83l0.987-0.298v1.128h1.108V9.598z"/>
<path fill="#FFFFFF" d="M75.033,11.07c0,0.696-0.198,1.268-0.596,1.715c-0.417,0.46-0.97,0.69-1.659,0.69
c-0.665,0-1.194-0.221-1.589-0.662c-0.396-0.441-0.592-0.998-0.592-1.668c0-0.702,0.203-1.277,0.61-1.724
c0.406-0.447,0.955-0.671,1.645-0.671c0.665,0,1.199,0.221,1.604,0.662C74.841,9.84,75.033,10.393,75.033,11.07z M73.989,11.103
c0-0.417-0.09-0.775-0.27-1.075c-0.212-0.361-0.513-0.542-0.904-0.542c-0.404,0-0.712,0.181-0.923,0.542
c-0.18,0.299-0.271,0.664-0.271,1.093c0,0.418,0.091,0.776,0.271,1.075c0.218,0.361,0.522,0.542,0.913,0.542
c0.386,0,0.687-0.184,0.904-0.551C73.896,11.882,73.989,11.52,73.989,11.103z"/>
<path fill="#FFFFFF" d="M80.299,13.372h-1.006v-2.594c0-0.8-0.305-1.2-0.914-1.2c-0.298,0-0.54,0.11-0.727,0.331
c-0.187,0.22-0.279,0.479-0.279,0.776v2.687h-1.007v-3.234c0-0.398-0.013-0.83-0.037-1.295h0.885l0.047,0.708h0.028
c0.118-0.22,0.292-0.402,0.521-0.547c0.273-0.169,0.578-0.254,0.914-0.254c0.422,0,0.773,0.137,1.053,0.41
c0.348,0.335,0.521,0.835,0.521,1.5V13.372z"/>
</g>
<g>
<path fill="#FFFFFF" d="M47.046,19.04c0,0.382-0.125,0.691-0.375,0.928c-0.25,0.237-0.573,0.355-0.968,0.355
c-0.355,0-0.655-0.122-0.899-0.365s-0.365-0.549-0.365-0.918c0-0.368,0.125-0.671,0.375-0.908c0.25-0.237,0.559-0.355,0.928-0.355
c0.368,0,0.678,0.118,0.928,0.355C46.921,18.368,47.046,18.671,47.046,19.04z M46.809,31.502h-2.133v-9.599h2.133V31.502z"/>
<path fill="#FFFFFF" d="M58.283,19.988h-3.811v11.514h-2.133V19.988h-3.792V18.19h9.736V19.988z"/>
<path fill="#FFFFFF" d="M67.171,31.502h-1.876l-0.118-1.461h-0.04c-0.672,1.119-1.686,1.678-3.041,1.678
c-0.948,0-1.705-0.296-2.271-0.889c-0.672-0.724-1.008-1.816-1.008-3.278v-5.648h2.134v5.293c0,1.844,0.632,2.765,1.896,2.765
c0.948,0,1.606-0.461,1.976-1.382c0.092-0.237,0.138-0.507,0.138-0.81v-5.866h2.133v6.833
C67.092,29.646,67.118,30.567,67.171,31.502z"/>
<path fill="#FFFFFF" d="M78.39,31.502h-2.133v-5.496c0-1.695-0.646-2.542-1.936-2.542c-0.633,0-1.146,0.233-1.541,0.699
c-0.395,0.466-0.593,1.015-0.593,1.646v5.693h-2.133v-6.854c0-0.842-0.026-1.758-0.079-2.745h1.877l0.099,1.501h0.059
c0.25-0.467,0.619-0.854,1.106-1.158c0.579-0.36,1.225-0.54,1.936-0.54c0.896,0,1.639,0.29,2.231,0.869
c0.737,0.711,1.106,1.771,1.106,3.18V31.502z"/>
<path fill="#FFFFFF" d="M89.271,26.248c0,0.383-0.026,0.705-0.079,0.969h-6.399c0.026,0.947,0.336,1.672,0.929,2.172
c0.54,0.447,1.237,0.672,2.094,0.672c0.947,0,1.81-0.151,2.587-0.455l0.336,1.482c-0.909,0.395-1.982,0.592-3.22,0.592
c-1.487,0-2.656-0.438-3.505-1.313c-0.85-0.876-1.274-2.05-1.274-3.525c0-1.448,0.396-2.653,1.186-3.614
c0.829-1.027,1.948-1.541,3.357-1.541c1.382,0,2.429,0.513,3.14,1.541C88.988,24.043,89.271,25.051,89.271,26.248z M87.237,25.695
c0.013-0.632-0.126-1.178-0.415-1.639c-0.369-0.593-0.935-0.889-1.698-0.889c-0.698,0-1.265,0.289-1.698,0.869
c-0.356,0.461-0.566,1.014-0.633,1.658H87.237z"/>
<path fill="#FFFFFF" d="M97.508,28.744c0,0.893-0.329,1.609-0.987,2.147c-0.659,0.538-1.567,0.808-2.726,0.808
c-1.094,0-2.021-0.217-2.785-0.651l0.454-1.58c0.737,0.448,1.521,0.671,2.351,0.671c1.093,0,1.639-0.4,1.639-1.204
c0-0.355-0.118-0.648-0.355-0.879c-0.236-0.23-0.658-0.458-1.264-0.682c-1.712-0.632-2.567-1.554-2.567-2.765
c0-0.83,0.316-1.521,0.948-2.074s1.468-0.829,2.508-0.829c0.948,0,1.758,0.193,2.43,0.58l-0.454,1.533
c-0.619-0.368-1.271-0.554-1.956-0.554c-0.447,0-0.797,0.105-1.046,0.316c-0.251,0.209-0.376,0.479-0.376,0.807
c0,0.329,0.132,0.599,0.396,0.809c0.224,0.197,0.658,0.414,1.304,0.65C96.678,26.479,97.508,27.443,97.508,28.744z"/>
</g>
</g>
</svg>
Then modify pages/preview/preview.html to use the
image, and link the user to iTunes.
<ion-header>
<ion-navbar>
<h3>{{tune.title}} — {{tune.artist}}</h3>
</ion-navbar>
</ion-header>
<ion-content padding>
<video autoplay>
<source src="{{tune.video}}">
</video>
<a href="{{tune.store}}" target="_blank">
<img src="assets/imgs/get-it-on-itunes.svg" />
</a>
</ion-content>And finally, style things to center the button and give
the page a black background. Edit pages/preview/preview.scss
and use this styling.
page-preview {
ion-header {
ion-navbar h3 {
font-size: 1.1em;
}
}
.content-md {
background-color: black;
}
ion-content {
video {
width: 100%;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
width: 40%;
}
}
}In this lab you did the kind of refactoring you'd do in any app:
ionic serve
ionic lab
card.io.cordova.mobilesdk 2.1.0 "CardIO"
com-intel-security-cordova-plugin 2.0.3 "APP Security API"
com.darktalker.cordova.screenshot 0.1.5 "Screenshot"
com.paypal.cordova.mobilesdk 3.5.0 "PayPalMobile"
cordova-admob-sdk 0.8.0 "AdMob SDK"
cordova-base64-to-gallery 4.1.2 "base64ToGallery"
cordova-instagram-plugin 0.5.5 "Instagram"
cordova-launch-review 2.0.0 "Launch Review"
cordova-plugin-3dtouch 1.3.5 "3D Touch"
cordova-plugin-actionsheet 2.3.3 "ActionSheet"
cordova-plugin-add-swift-support 1.6.2 "AddSwiftSupport"
cordova-plugin-admob-free 0.10.0 "Cordova AdMob Plugin"
cordova-plugin-advanced-http 1.8.1 "Advanced HTTP plugin"
cordova-plugin-app-event 1.2.0 "Application Events"
cordova-plugin-apprate 1.3.0 "AppRate"
cordova-plugin-battery-status 1.2.4 "Battery"
cordova-plugin-ble-central 1.1.4 "BLE"
cordova-plugin-bluetooth-serial 0.4.7 "Bluetooth Serial"
cordova-plugin-brightness 0.1.5 "Brightness"
cordova-plugin-calendar 4.6.0 "Calendar"
cordova-plugin-camera 2.4.1 "Camera"
cordova-plugin-compat 1.1.0 "Compat"
cordova-plugin-contacts 2.3.1 "Contacts"
cordova-plugin-datepicker 0.9.3 "DatePicker"
cordova-plugin-device 1.1.6 "Device"
cordova-plugin-device-motion 1.2.5 "Device Motion"
cordova-plugin-device-orientation 1.0.7 "Device Orientation"
cordova-plugin-dialogs 1.3.3 "Notification"
cordova-plugin-email-composer 0.8.7 "EmailComposer"
cordova-plugin-geolocation 2.4.3 "Geolocation"
cordova-plugin-globalization 1.0.7 "Globalization"
cordova-plugin-health 1.0.0 "Cordova Health"
cordova-plugin-image-picker 1.1.1 "ImagePicker"
cordova-plugin-inappbrowser 1.6.1 "InAppBrowser"
cordova-plugin-insomnia 4.3.0 "Insomnia (prevent screen sleep)"
cordova-plugin-ionic 1.1.8 "IonicCordova"
cordova-plugin-ios-keychain 3.0.1 "KeyChain Plugin for Cordova iOS"
cordova-plugin-media 3.0.1 "Media"
cordova-plugin-mixpanel 3.1.0 "Mixpanel"
cordova-plugin-music-controls 2.0.0 "MusicControls"
cordova-plugin-nativeaudio 3.0.9 "Cordova Native Audio"
cordova-plugin-nativestorage 2.2.2 "NativeStorage"
cordova-plugin-network-information 1.3.3 "Network Information"
cordova-plugin-request-location-accuracy 2.2.1 "Request Location Accuracy"
cordova-plugin-safariviewcontroller 1.4.7 "SafariViewController"
cordova-plugin-screen-orientation 2.0.1 "Screen Orientation"
cordova-plugin-secure-storage 2.6.8 "SecureStorage"
cordova-plugin-shake 0.6.0 "Shake Gesture Detection"
cordova-plugin-sim 1.3.3 "SIM"
cordova-plugin-splashscreen 4.0.3 "Splashscreen"
cordova-plugin-statusbar 2.2.4-dev "StatusBar"
cordova-plugin-stripe 1.5.3 "cordova-plugin-stripe"
cordova-plugin-taptic-engine 2.1.0 "Taptic Engine"
cordova-plugin-themeablebrowser 0.2.17 "ThemeableBrowser"
cordova-plugin-touch-id 3.2.0 "Touch ID"
cordova-plugin-tts 0.2.3 "TTS"
cordova-plugin-vibration 2.1.5 "Vibration"
cordova-plugin-whitelist 1.3.2 "Whitelist"
cordova-plugin-x-socialsharing 5.1.8 "SocialSharing"
cordova-plugin-x-toast 2.6.0 "Toast"
cordova-plugin-zip 3.1.0 "cordova-plugin-zip"
cordova-promise-polyfill 0.0.2 "cordova-promise-polyfill"
cordova-sms-plugin 0.1.11 "Cordova SMS Plugin"
cordova-sqlite-storage 2.0.4 "Cordova sqlite storage plugin"
cordova-universal-clipboard 0.1.0 "Clipboard"
de.appplant.cordova.plugin.local-notification 0.8.5 "LocalNotification"
de.appplant.cordova.plugin.printer 0.7.1 "Printer"
ionic-plugin-keyboard 2.2.1 "Keyboard"
phonegap-plugin-barcodescanner 6.0.7 "BarcodeScanner"
phonegap-plugin-mobile-accessibility 1.0.5-dev "Mobile Accessibility"
uk.co.workingedge.phonegap.plugin.launchnavigator 4.0.4 "Launch Navigator"See https://ionicframework.com/docs/pro/devapp/
| Run type | Developers | UI testers |
|---|---|---|
| ionic serve | ||
| ionic lab | ||
| DevApp | ||
| .ipa or .apk | ✔︎ | ✔︎ |
✔︎
✔︎
✔︎
✔︎
There are several ways to run your app:
Developers most often use the first three, and you've already
been using
ionic serve. In this lab you'll try
ionic lab
and DevApp.
ionic lab
Use a terminal window to navigate to the
itunes directory
and run
ionic lab
Like
serve,
ionic lab builds your app and starts a server
and opens a terminal window running your app.
(It tries to use port 8100, and if that port is being used, it uses the next available port. Since you're
ionic serve is
probably already running,
lab will probably use 8101.)
In the new browser window, try running the app with different combinations of iOS, Android and Windows.
When you're finished trying things out, cancel out of the
ionic lab in terminal, and close the browser window.
Install Ionic DevApp on your smartphone from either the Apple app store or from Google Play.
Make sure your smartphone is on the same WiFi network as your development machine, then open DevApp. You'll see all the apps
running on the network. These apps must be running via
ionic serve or
ionic lab.
Look for your machine, and run it.
While looking at your app running in DevApp, edit
app/components/list-item/list-item.html and change the artist's name to use an
<h1>. Save your change
and you'll see the change automatically reflected in DevApp.
|
|
DevApp is a great way for you to see how your app looks and behaves on a device. You can also test your native plugins.
<ion-list reorder="true">
<ion-item *ngFor="let item of items">{{ item }}</ion-item>
</ion-list><ion-list>
<ion-list-header>Header</ion-list-header>
<ion-item-group reorder="true">
<ion-item *ngFor="let item of items">{{ item }}</ion-item>
</ion-item-group>
</ion-list><ion-list reorder="true">
<ion-list-header>Header</ion-list-header>
<ion-item *ngFor="let item of items">{{ item }}</ion-item>
</ion-list><ion-list>
<ion-list-header>Header</ion-list-header>
<ion-item-group reorder="true" (ionItemReorder)="reorderItems($event)">
<ion-item *ngFor="let item of items">{{ item }}</ion-item>
</ion-item-group>
</ion-list>
class MyComponent {
items = [];
constructor() {
for (let x = 0; x < 5; x++) {
this.items.push(x);
}
}
reorderItems(indexes) {
let element = this.items[indexes.from];
this.items.splice(indexes.from, 1);
this.items.splice(indexes.to, 0, element);
}
}import { reorderArray } from 'ionic-angular';
class MyComponent {
items = [];
constructor() {
for (let x = 0; x < 5; x++) {
this.items.push(x);
}
}
reorderItems(indexes) {
this.items = reorderArray(this.items, indexes);
}
}<ion-list>
<ion-list-header>Header</ion-list-header>
<ion-item-group reorder="true" (ionItemReorder)="$event.applyTo(items)">
<ion-item *ngFor="let item of items">{{ item }}</ion-item>
</ion-item-group>
</ion-list><ion-list>
<ion-item-sliding #item>
<ion-item>
Item
</ion-item>
<ion-item-options side="left">
<button ion-button (click)="favorite(item)">Favorite</button>
<button ion-button color="danger" (click)="share(item)">Share</button>
</ion-item-options>
<ion-item-options side="right">
<button ion-button (click)="unread(item)">Unread</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>In this lab you'll add a feature that lets the user like or dislike a video, using a slider with two buttons.
You may wish to refer to lecture slides, and the StackBlitz sliding item example as you work the lab.
Edit components/list-item/list-item.html and surround
the list item with an <ion-item-sliding #item> element.
Note the #item — that puts a reference on the element,
which allows you to pass it to the sliding button click
events.
Then within the sliding item element, add an <ion-item-options> that contains two buttons.
<ion-item-options side="right">
<button ion-button icon-only color="light">
<ion-icon name="heart"></ion-icon>
</button>
<button ion-button icon-only color="light">
<ion-icon name="remove"></ion-icon>
</button>
</ion-item-options>
In your browser, try things out by swiping left, and tapping on the heart or remove button.
The video plays! But you don't want that, instead, you want the click to set whether or not the user likes the music video. To solve the problem you need to set up events on the two sliding buttons, and those event handler need to prevent the event from being propogated to other elements higher in the DOM.
Edit the heart button and have its click event run a method
named onLikeClick($event, item). The first parameter
is the mouse event. The second is a reference to the
ion-item-sliding.
Then add the method to components/list-item/list-item.ts. The first parameter is of type MouseEvent and the second of type ItemSliding. Since you're using ItemSliding, you'll
need to import it.
import { ItemSliding } from "ionic-angular";To prevent the event from propogating, add this statement to
the onLikeClick method:
event.stopPropagation();Then add a second statement that closes the sliding item:
item.close();Clicking the like button should close the sliding item, and should no longer play the video.
Once you get the like button working, do the same for the
other button. Have it run a method named onRemoveLikeClick,
passing the same parameters. The method in list-item.ts
should have the same two statements as onLikeClick.
Both buttons should now work the same.
Edit components/list-item/list-item.scss and add replace
the contents with this styling.
list-item {
ion-thumbnail {
position: relative;
img {
width: auto !important;
height: auto !important;
position: absolute;
top: 50%;
transform: translate(0, -50%);
}
}
ion-icon[name="heart"] {
color: red;
}
}As you can see, the styling selects ion icons which use
heart="name" attrubute, and set its color to red.

export interface Tune {
artist : string;
title : string;
thumbnail : string;
video : string;
store : string;
like? : boolean;
}
In this lab you'll add add logic to mark a Tune as a favorite.
Edit
components/list-item/list-item.ts and edit
the
onLikeClick event and have it set
tune.like
to true.
this.tune.like = true;
At this point you'll get an error! That's because when
you defined the
Tune interface, you didn't include
a
like property.
Edit
providers/itunes/tune.js and add an optional
like property.
export interface Tune {
artist: string;
title: string;
thumbnail: string;
video: string;
store: string;
like?: boolean;
}
Save your changes, and the linter error in
list-item.ts
should be gone.
Now edit
list-item.ts and have the
onRemoveLikeClick
method set
this.tune.like = false;.
You're updating the tune object, but the user interface doesn't reflect that choice.
Edit
components/list-item/list-item.html and edit the
<h1>
that shows the song title. You'll use an
*ngIf to
show a heart if the song is a favorite.
<h1>
<ion-icon *ngIf="tune.like" name="heart"></ion-icon>
{{tune.title}}
</h1>
const storage = window.localStorage; // Or window.sessionStorage
storage.setItem('foo', 'bar'); // Sets a key/value pair
storage.getItem ('foo'); // Gets the value of foo
storage.foo; // Same as storage.getItem('foo')
storage['foo'];storage.foo = 'bar'; // Same as storage.setItem('foo', 'bar');
storage['foo'] = 'bar';
storage.length; // Number of key-value pairs
storage.key(2); // The second key (zero-based)storage.removeItem ('foo'); // Removes the entry
delete storage.foo;
delete storage['foo'];
storage . clear(); // Removes all entries
In this lab you'll persist which tunes are liked.
The app reflects your favorites, but every time the app refreshes the data is refetched, and the favorites are lost.
In theory, you'd code some kind of backend service to persist your favorites. But to keep things simple, we'll just keep that information in local storage.
We could put the persistence code in the provider, or in the view. We'll put it in the provider.
You're going to add a setLike(Tune,boolean) method
to the provider. The setter will handle updating
the tune, as well as persisting the data.
This sounds like another job for... the intern.
Here's the new version of providers/itunes/itunes.ts.
Completely replace the provider with this code.
import { Injectable } from "@angular/core";
import axios from "axios";
import { Tune } from "./tune";
@Injectable()
export class ItunesProvider {
async get(): Promise {
const me = this;
let url = "https://itunes.apple.com/us/rss/topmusicvideos/limit=50/json";
const response = await axios.get(url);
const result: Tune[] = response.data.feed.entry.map(item => {
const result: Tune = {
artist: item["im:artist"].label,
title: item.title.label,
thumbnail: item["im:image"][2].label,
video: item.link[1].attributes.href,
store: item.link[0].attributes.href
};
result.like = this.favorites.has(this.getTuneKey(result));
return result;
});
result.sort((a: Tune, b: Tune) => {
return a.artist.localeCompare(b.artist) || a.title.localeCompare(b.title);
});
return result;
}
constructor() {
console.log("Hello ItunesProvider Provider");
const s = localStorage.getItem("favorites");
this.favorites = new Set(s ? JSON.parse(s) : []);
console.log(s);
}
private favorites: Set;
public setLike(tune: Tune, like: boolean) {
// Store
const key = this.getTuneKey(tune);
tune.like = like;
if (tune.like) {
this.favorites.add(key);
} else {
this.favorites.delete(key);
}
localStorage.setItem(
"favorites",
JSON.stringify(Array.from(this.favorites))
);
}
private getTuneKey(tune: Tune) {
// We need some unique identifier. The store URL should be unique.
return tune.store;
}
}
Now edit components/list-item/list-item.ts.
First, import the provider.
import { ItunesProvider } from "../../providers/itunes/itunes";Then modify the constructor to inject it.
constructor(private iTunesProvider: ItunesProvider) {
console.log("Hello ListItemComponent Component");
}Now you need to modify the onLikeClick and onRemoveLikeClick events to use the new setLike method.
Change onLikeClick and replace the statement that sets like to true with
this.iTunesProvider.setLike(this.tune, true);Change onLikeClick and replace the statement that sets like to false with
this.iTunesProvider.setLike(this.tune, false);Now like some songs, and refresh the browser. Those selections should be preserved. Remove the like for a few and refresh again, then verify that the likes are marked accordingly,
In this lab you did used a sliding item to mark tunes as favorites. Then you modified the provider to persist the information.
<button ion-button>
Button
</button><ion-card>
<ion-card-header>
This is the header
</ion-card-header>
<ion-card-content>
Card content goes here
</ion-card-content>
</ion-card>Basics
Lists in cards
Creativity
<body>
<ion-app></ion-app>
<script src="build/polyfills.js"></script>
<!-- node_modules -->
<script src="build/vendor.js"></script>
<!-- Your app's code -->
<script src="build/main.js"></script>
</body>
* Although by this point the code has gone through the TypeScript and Angular transmogrification. ;-)
In this lab you'll learn the procedure for setting up a lazily loaded app.
To that end you'll create two tabbed starter apps: one with and one without lazy loading.
Use a terminal window and navigate to the IonicTraining
folder, and create a new starter app using this command.
ionic start eagertabs tabsDo not integrate it with Cordova and do not set it up in Pro.
home.html
To make it clear which app you're running, edit eagertabs/src/pages/home/home.html and replace the <ion-content> with this code.
<ion-content padding>
<h1>Eager</h1>
</ion-content>Use a terminal window and navigate to the IonicTraining/eagertabs directory and run the app via ionic serve.
In the debugger, look at network traffic and note that main.js is loaded immediately, and there are no additional
.js files loaded as you visit the three tabs.

Note the time to load (and parse) and size of main.js. They
should be something like 16.5KB and 174ms.
You'll now contrast the behavior of the eager version with a lazily-loaded version.
Use a termial window and navigate to IonicTraining
and run
ionic start lazytabs tabsDo not integrate it with Cordova and do not set it up in Pro.
From now on you'll only be editing the lazy-loading version
of the app., so make sure you're editing in the lazytabs directory.
To differentiate this version from the eager version,
edit lazytabs/src/pages/home/home.html and replace the
<ion-content padding>
<h1>Lazy</h1>
</ion-content>Create the file pages/home/home.module.ts and use this
content.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { HomePage } from './home';
@NgModule({
declarations: [HomePage],
imports: [IonicPageModule.forChild(HomePage)],
})
export class HomePageModule { }Then create a module for the other pages.
pages/contact/contact.module.tspages/about/about.module.tspages/tabs/tabs.module.tsYou can just copy-and-paste from home.module.ts and
modify the import and the module's declarations and
imports arrays, and use the corresponding page name.
Edit each page — about.ts, contact.ts, home.ts and tabs.ts — and add two statements.
import { IonicPage } from "ionic-angular";@IonicPage() (before the class declaration, before the @Component)For example, here's what contact.ts will look like.
import { Component } from "@angular/core";
import { NavController } from "ionic-angular";
import { IonicPage } from "ionic-angular";
@IonicPage()
@Component({
selector: "page-contact",
templateUrl: "contact.html"
})
export class ContactPage {
constructor(public navCtrl: NavController) {}
}Edit app/app.module.ts and remove all references to the
pages:
import statements for the pagesdeclarations and imports
app.component.ts
The root of the <ion-nav> is specified in app.component.ts.
Edit that file, and remove the import for TabsPage.
Then change the initialization of rootPage to use the
string name of TabsPage, rather than the class itself.
rootPage: any = "TabsPage";pages/tabs/tabs.ts
Similarly to app.component.ts naming the root page,
tabs.ts names the tabs.
Edit pages/tabs/tabs.ts, and remove the imports for
the three pages. Then change the page initializations
to use the string names of the pages, rather than the
classes themselves.
tab1Root = "HomePage";
tab2Root = "AboutPage";
tab3Root = "ContactPage";In the running app, note that main.js is now smaller,
and consequently, the load time is quicker.

Furthermore,
you should see two other .js files initially loaded.
These hold the tabs page and home page. Then select the
About and Contacts tabs, and you should see a .js
page dynamically loaded for each tab.

"dependencies": {
"@angular/common": "5.0.3",
"@angular/compiler": "5.0.3",
"@angular/compiler-cli": "5.0.3",
"@angular/core": "5.0.3",
"@angular/forms": "5.0.3",
"@angular/http": "5.0.3",
"@angular/platform-browser": "5.0.3",
"@angular/platform-browser-dynamic": "5.0.3",
"@ionic-native/core": "4.4.0",
"@ionic-native/splash-screen": "4.4.0",
"@ionic-native/status-bar": "4.4.0",
"@ionic/storage": "2.1.3",
"axios": "^0.18.0",
"ionic-angular": "3.9.2",
"ionicons": "3.0.0",
"rxjs": "5.5.2",
"sw-toolbox": "3.6.0",
"zone.js": "0.8.18"
},
"devDependencies": {
"@ionic/app-scripts": "3.1.8",
"typescript": "2.4.2"
},
Here's a sample
Dependent libraries are included in the build
There's also a section for development dependencies
In this case, the dependencies ask for specific versions of the libraries
But you can set thins up to allow later versions, as they become available
"@ionic-native/core":"4.4.0"
"@ionic-native/core":"*"
"@ionic-native/core":"4.4.*"
"@ionic-native/core":"4.*"
* x
"@ionic-native/core":"<4.2"
"@ionic-native/core":"3-4.2"
- < <= > >=
"@ionic-native/core":"~4.2.5"
Patch releases are ok
~
">4.2.5 < 4.3"
"@ionic-native/core":"^4.2.5"
Minor version releases are ok
^
">4.2.5 < 5"
npm outdated
npm outdated
Package Current Wanted Latest Location
@angular/common 5.0.3 5.0.3 5.2.6 itunes
@angular/compiler 5.0.3 5.0.3 5.2.6 itunes
@angular/compiler-cli 5.0.3 5.0.3 5.2.6 itunes
@angular/core 5.0.3 5.0.3 5.2.6 itunes
@angular/forms 5.0.3 5.0.3 5.2.6 itunes
@angular/http 5.0.3 5.0.3 5.2.6 itunes
@angular/platform-browser 5.0.3 5.0.3 5.2.6 itunes
@angular/platform-browser-dynamic 5.0.3 5.0.3 5.2.6 itunes
@ionic-native/core 4.4.0 4.4.0 4.5.3 itunes
@ionic-native/splash-screen 4.4.0 4.4.0 4.5.3 itunes
@ionic-native/status-bar 4.4.0 4.4.0 4.5.3 itunes
rxjs 5.5.2 5.5.2 5.5.6 itunes
typescript 2.4.2 2.4.2 2.7.2 itunes
zone.js 0.8.18 0.8.18 0.8.20 itunes
In this lab you'll generate the starter app for the ISS Tracker.
First, generate the starter with mobile integration.
Use a terminal window and navigate to the IonicTraining
and enter this command:
ionic start iss tabs
Respond Yes when prompted Would you like to integrate your new app with Cordova...
Among other things, answering yes creates the config.xml used by Cordova.
Respond no when prompted Install the free Ionic Pro SDK and connect your app?
Use a code editor, and open iss/packages.json and look
at the dependencies entry. Your copy of packages.json should look something like this.
"dependencies": {"@angular/animations": "5.2.9","@angular/common": "5.0.3","@angular/compiler": "5.0.3","@angular/compiler-cli": "5.0.3","@angular/core": "5.0.3","@angular/forms": "5.0.3","@angular/http": "5.0.3","@angular/platform-browser": "5.0.3","@angular/platform-browser-dynamic": "5.0.3","@ionic-native/core": "4.4.0","@ionic-native/splash-screen": "4.4.0","@ionic-native/status-bar": "4.4.0","@ionic/storage": "2.1.3","ionic-angular": "3.9.2","ionicons": "3.0.0","rxjs": "5.5.2","sw-toolbox": "3.6.0","zone.js": "0.8.18"},
To see what's versions are out there, use your terminal
window and navigate to the iss directory and run this command.
npm outdated
You should see something like this.
$ npm outdatedPackage Current Wanted Latest Location@angular/common 5.0.3 5.0.3 5.2.6 iss@angular/compiler 5.0.3 5.0.3 5.2.6 iss@angular/compiler-cli 5.0.3 5.0.3 5.2.6 iss@angular/core 5.0.3 5.0.3 5.2.6 iss@angular/forms 5.0.3 5.0.3 5.2.6 iss@angular/http 5.0.3 5.0.3 5.2.6 iss@angular/platform-browser 5.0.3 5.0.3 5.2.6 iss@angular/platform-browser-dynamic 5.0.3 5.0.3 5.2.6 iss@ionic-native/core 4.4.0 4.4.0 4.5.3 iss@ionic-native/splash-screen 4.4.0 4.4.0 4.5.3 iss@ionic-native/status-bar 4.4.0 4.4.0 4.5.3 issrxjs 5.5.2 5.5.2 5.5.6 isstypescript 2.4.2 2.4.2 2.7.2 isszone.js 0.8.18 0.8.18 0.8.20 iss
As you can see, there are later versions of most of the packages we're using.
The entries are of the form [major version].[minor version].[patch], and use semantic versioning, or semver, to specify which package versions can be used. If you were to prefix an entry with ~, you're telling
npm that you will accept a patch release. For example, ~5.1.0 means your app can use 5.1.2 but not 5.2.0.
The ^ prefix means you'll accept a minor release.
In the case of ISS, you want to specify the a newer minor release of everything.
Edit packages.json and add a tilde — ~ — before
each version specification in the dependencies property.
The result will look something like this:
"dependencies": {"@angular/animations": "~5.2.9","@angular/common": "~5.2.6","@angular/compiler": "~5.2.6","@angular/compiler-cli": "~5.2.6","@angular/core": "~5.2.6","@angular/forms": "~5.2.6","@angular/http": "~5.2.6","@angular/platform-browser": "~5.2.6","@angular/platform-browser-dynamic": "~5.2.6","@ionic-native/core": "~4.5.0","@ionic-native/splash-screen": "~4.5.0","@ionic-native/status-bar": "~4.5.0","@ionic/storage": "~2.1.3","ionic-angular": "~3.9.2","ionicons": "~3.0.0","rxjs": "~5.5.2","sw-toolbox": "~3.6.0","zone.js": "~0.8.18"},
Save your changes. Then ask npm to install the specified versions by entering this in a terminal window.
npm install
On the terminal console you'll see what new versions were installed. Depending on what new versions may exist the day you create the project, you may see several or no new packages installed.
Use a terminal window and navigate to the iss
directory, and run
ionic serve
As you know, this starts the server on port 8100 and opens a terminal window.
The app looks like this (with the debugger docked on the right).

In this lab you generated the ISS starter app, and installed up-to-date versions of various npm libraries.
Monitor
Error reporting
Deploy
Live updates
Package
Native builds
Your Local Ionic Project
Your Local Ionic Project
git push ionic master
Your Local Ionic Project
git push ionic master
In this lab you'll use configure ISS to automatically obtain new versions of your app.
Open a terminal window and navigate to the iss directory,
then enter ionic login. When promted, use your Pro email and password.
Log in to Ionic Pro and create a new personal app named iss.
Then use the left navigation and go to Code > Builds and choose the Connect your app to Ionic Pro option. That will redirect you to the Settings > Git page. Note the instructions for linking an existing app.

Copy the ionic link command (including the app ID), then open a terminal window in the iss
directory and paste and run the command. When prompted with Which git host would you like to
use?, choose Ionic Pro.
Use a terminal window in iss and do a git push.
git push ionic master
If you wait a moment, then refresh the Ionic Pro dashboard, you should see the commit being built.

In the Pro Dashboard, go to iss > Channels > Production then click on the Setup Deploy button to the right.
Then in the Setup Deploy dialog, choose Download updates in splashscreen and install immediately option from the dropdown. Copy the Cordova statement

Note that by default the user is shown a dialog when the plugin detect a new version of the app. That can be suppressed via an additional parameter, although there's no need to use that for the ISS app.
--variable WARN_DEBUG="false"
In a terminal window open in iss, paste the Codova code,
and run the command.
The command updates config.xml —
open config.xml and scroll down to the bottom and you
should see the specificaiton for the plugin. It should look
something like this, although the details may differ.
<plugin name="cordova-plugin-ionic" spec="^4.1.7"><variable name="APP_ID" value="99ca5a00" /><variable name="CHANNEL_NAME" value="Production" /><variable name="UPDATE_METHOD" value="auto" /><variable name="UPDATE_API" value="https://api.ionicjs.com" /><variable name="MAX_STORE" value="2" /></plugin>
Deploy is now set up! If you were to build and run your app on a device, then assign a new build of the app to the Production channel, the app would use that the next time it runs.
From now on, you should do a git push at the end of each lab.
Run these commands.
git add .git commit -m "Setup Deploy"git push ionic master
In this lab you'll do a build, then run the app on a phone.
In this lab you'll:
Cordova looks at config.xml when it builds your application.
That file is created when you generate the starter app (if you respond yes
to the question about Cordova integration). It's also created
if you run various Cordova-related cli commands.
The details of how Cordova uses config.xml are
documented here.
Use a code editor to open config.xml and change the
<name>, <description>, and <author> elements, as
well as the id attribute.
The ID is particularly important because it's used as the
bundle identifier in the App Store, and as the
applicationId in Google Play. It must be unique among
all apps in those stores. By convention, the ID is in reverse
domain format. For example, if the app runs at iss.mydomain.com,
then you might use com.mydomain.iss as
the ID.
<widget> element to be of the form id="com.yourname.iss", using your surname<name> to ISS Tracker
<descripton> to a one sentence description of the app<author> to your nameOpen a terminal window and navigate to the iss directory,
then enter one of these commands, depending on your
mobile device.
ionic cordova platform add iosionic cordova platform add android
This adds plaform setup infrastructure in the iss/platforms/ios or
iss/platforms/android folder.
Your project has a resources/icon.png and resources/splash.png that hold a
default icon and splash page (featuring the Ionic logo). The icon file is 1024/1024
pixels, and the splash image is 2723 x 2713 pixels.
There's are also resources/ios/icon, resources/ios/splash,
resources/android/icon, and resources/android/splash
directories. If you look in them, they contain many versions of the icon and
splash page, suitable for different device aspect ratios and orientations.
You should use your own icon and splash page!
To create an icon and splash screen for your
application, simply replace the logo.png and splash.png
images, then ask Ionic to generate the assocated set of images.
That's done via ionic cordova resources.
Get a copy of this splash page iage and replace iss/resources/splash.png. tap
on the image link, which opens it in a new browser window. Then right click
and Save as....
Then replace iss/resources/icon.png with this icon. Follow the image link, which opens
it in a new browser window. Then choose right click and Save as....
Then use a terminal window and navigate to your iss
directory, and run this command.
ionic cordova resources
The command requires that you log in using your Ionic account. You did this earlier, when you linked your app to your Pro account, so you should already be logged in.
(Note: These sizes are minimum sizes and are subject to change as new devices are released. You can make the images larger, but you might have scaling issues. The splash screen will be cropped and scaled, so it is best to put any logos or other images in the center.)
Now that the icons and splash page images are ready, you can build and run the application.
Use a terminal window and run one of these commands, depending on your mobile device.
ionic cordova build iosionic cordova build android
Pro tip: If you're ever worried that something in your environment may be out of sync, do this to make a clean build:
node_modules directorywww directorynpm install to get a fresh copy of the node node_modulesionic cordova build to create a fresh copy of www
Once the build is finished, you are free to install the app on your device.
When the build is finished, connect your phone to your computer.
Then open the iss/platforms/ios
directory in Xcode and do a few things:

Xcode should then install and run the app on your device.
You may get a warning asking you to Verify the Developer App certificate for your account is trusted on your device.

If you see that message, on your iPhone Choose Settings > General > Profiles & Device Management > Developer App then tap the Trust button.
You should now see the app icon on your iPhone, and tapping it launches the app.
When you did the build in the earlier step, an apk is placed somewhere
in the iss/platforms/android directory. Different Cordova releases may
put the apk in different places, but in Cordova 8 it's placed in
iss/platforms/android/app/build/outputs/app/debug.apk.
To run it on your device, you'll need to tell Android to allow the installing of apps from a source other than the app store. To do that, on the device open Menu > Settings > Security and enable the Unknown sources option.
You may also need to enable USB debugging via Menu > Settings > Developer options and enable USB Debugging.
Android provides a command-line tool called the Android Debug Bridge (adb) which helps communicate with an attached device and do installs and debugging. To use it read the instructions at https://developer.android.com/studio/command-line/adb#move
Some people just put the apk in DropBox, and open the
link from their Android phones.
While the application is running, you can monitor and debug it via Safari (for iOS) or Chrome (for Android).
For information on using a browser to debug your applications, see our helpful guide for debugger tips.
Everything is in place to see Deploy in action:
This means that any build assigned to the Production channel will automatically be seen as you run the app on your phone.
Verify that you see the starter Welcome to Ionic app. Then close the app.

Edit pages/home/home.html and replace it with this HTML.
<ion-header>
<ion-navbar>
<ion-title>Home</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<button ion-button>This is a new version of the app</button>
</ion-content>Run the app locally via ionic serve and verify that
you see the new version.

Use a terminal window in the iss directory and run these
commands.
git add .
git commit -m "New blue button version"
git push ionic masterIn the Pro dashboard, change tabs or refresh the page, and see that the new commit is being built.
Wait for the build to complete, then choose iss > Code > Builds and choose the Assign build button. On the Assign Build dialog, choose Production from the dropdown, then click Deploy.

As a result, the build should show that it has been assigned to the Production channel.


It may take up to a minute or so for Pro to fully reflect the assignment. So wait a moment, then re-open the app on your phone. You should see the new version of the app! :-)
For the rest of class, at the end of each lab you'll do a git push and assign the builds to Production. That way you'll always see the latest version of your app running on your device. Note that in the case of the Apple store, Deploy is used for tweaks and bug fixes — versions of your app with significant changes in functionality, or changes in plugins, should be re-submitted to the app store for review.
The starter app has tabs, like we want for ISS, but the pages do not have the correct names, and the icons are wrong. Let's fix that.
In this lab you will:
The style of your application can be adjusted in a few ways.
src/theme/variables.scss
src/app/app.scss
scss fileFor this lab, you'll use a nicer color for the header.
To do this, update the src/theme/variables.sccs and
add a new line that defines a header color.
$colors: (
header: #6a1b9a,
primary: #488aff,
secondary: #32db64,
danger: #f53d3d,
light: #f4f4f4,
dark: #222
);The term for this construct is a Sass map, which as you can see, holds a set of semantically named color values.
Now that you have the new color value defined, you need to change the Sass variable Ionic that describes the boolbar header. Add this line after the colors map:
$toolbar-background: color($colors, header);When you save, Ionic will automatically detect the change and refresh the browser. You should see the header color change.
|
|
Ionic uses Sass variables to style components. The change
to toolbar-background replaces the defalt toolbar header
background with the new value.
The starter app's tab names and icons don't make sense for the ISS Tracker. Instead, we want the tabs named Map, Passes and Astronauts.
Ionic comes with scores of icons. Logical icons for the ISS location tab might the locate or map icon. The passes tab shows a list, so the list icons is good. And for the astronauts tab the people icon is good.
Use your code editor and open `src/pages/tabs/tabs.html
and replace the contents with this.
<ion-tabs>
<ion-tab [root]="tab1Root" tabTitle="Map" tabIcon="locate"></ion-tab>
<ion-tab [root]="tab2Root" tabTitle="Passes" tabIcon="list"></ion-tab>
<ion-tab [root]="tab3Root" tabTitle="Astronauts" tabIcon="people"></ion-tab>
</ion-tabs>![]()
At this point, there is a disconnect between the name of our pages in the code and what they're used for. Fixing that is a brute force operation where you need to change the file names and references to those names.
Changing the file names requires changing the directory names,
and the prefix for the html, ts and scss within
the folders. You'll change about to passes, contacts
to astronauts and home to map. You'll edit the ts
files to change the selector, template path, and class names.
This operation goes more smoothly if you do one page entirely, and verify that the application runs. Then move on to the second, and verify. Then the third.
| File names before | File names after | Change to .ts file |
|---|---|---|
about/
about.html
about.scss
about.ts
|
passes/
passes.html
passes.scss
passes.ts
|
selector:"page-passes" templateUrl: "passes.html" export class PassesPage |
contacts/
contacts.html
contacts.scss
contacts.ts
|
astronauts/
astronauts.html
astronauts.scss
astronauts.ts
|
selector:"page-astronauts" templateUrl: "astronauts.html" export class AstronautsPage |
home/
home.html
home.scss
home.ts
|
map/
map.html
map.scss
map.ts
|
selector:"page-map" templateUrl: "map.html" export class MapPage |
Once you've changed the file names and classes, you need to change the how those are referenced.
First, edit pages/tabs/tabs.ts and change the imports and
the assignments to tab1Root, tab2Root and tab2Root.
Then edit src/app/app.module.ts and change the
imports to use the new names and paths. Then change the declarations and entryComponents to use the new names.
You changed the starter tabs app to reflect what makes sense for ISS. You also changed the header styling.
The photo was taken by Dylan O’Donnell, an amateur astrophotographer living near Brisbane Australia
The first tab will eventually display a map showing the current location of the International Space Station.
In this lab you'll the map and show a marker. Later, you'll write the code that determines the current location of the ISS, and then you'll update the code to have the marker reflect the location.
In this lab you'll:
This step introduces the idea of lifecycle events as well as the use of alternate styles depending on the platform (iOS or Android).
Visit the Google developers dashboard and choose Create Project.
Name the project Ionic Training.
Then choose Enable APIs and Services and search for and enable the Google Maps Geocoding API and the Google Maps JavaScript API.

Then go to the credentials page and choose Create credentials > API key.

Copy your API key and keep it in a safe place — you'll be using that when you make calls to Google.

Note that Google lets you restrict your keys. For example, for development work you might want to restrict a key to localhost. We won't bother doing that, but if you continue to use the key you should set up some restrictions.
Use a terminal window and navigate to the iss directory
and enter this command.
npm i @types/googlemaps --saveThat installs the Google Maps library and updates your
package.json dependencies.
index.html
Use a code editor and open your project's src/index.html
file, and page this script tag as the last item within the
<head>. Use your API key in place of the YOUR_KEY_HERE.
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE"></script>If the API is not set up correctly, you'll get a run-time error and a console message stating Google Maps API error: InvalidKeyMapError.
If it is ok, the app will run as before, although you won't see the map yet — you'll add code to show the map in the next lab.
<!-- Sample from https://developers.google.com/maps/documentation/javascript/adding-a-google-map -->
<!DOCTYPE html>
<html>
<head>
<title>Simple Map</title>
<meta name="viewport" content="initial-scale=1.0">
<meta charset="utf-8">
<style>
/* Always set the map height explicitly. */
#map { height: 100%; }
html, body { height: 100%; margin: 0; padding: 0; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var map;
function initMap() {
map = new google.maps.Map(
document.getElementById('map') ,
{ center: {lat: -34.397, lng: 150.644 },
zoom: 8}
);
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"async defer></script>
</body>
</html>map = new google.maps.Map(
document.getElementById('map') ,
{ center: {lat: -34.397, lng: 150.644 },
zoom: 8}
);
map.setCenter({lat: -34, lng: 151});
map.panTo({lat: -34, lng: 151});
map.setZoom(4);
var marker = new google.maps.Marker({
position: {lat: -25.363, lng: 131.044};,
map: map
});In this lab you'll the map and show a marker. Later, you'll write the code that determines the current location of the ISS, and then you'll update the code to have the marker reflect the location.
Completely replace the contents of pages/map/map.html
with this.
<ion-header>
<ion-navbar>
<ion-title>Map</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<div id="iss-tracking-map"></div>
</ion-content>Note the <div id="iss-tracking-map"> — that's
where the map will appear.
Then completely replace pages/map/map.scss with this.
page-map {
#iss-tracking-map {
height: 100%;
width: 100%;
}
}That makes the map element take up the full height and width of the page.
Then completely replace pages/map/map.ts with this.
import { Component } from "@angular/core";
import { NavController } from "ionic-angular";
@Component({
selector: "page-map",
templateUrl: "map.html"
})
export class MapPage {
constructor(public navCtrl: NavController) {}
ionViewDidLoad() {
this.pan({ latitude: 43.074237, longitude: -89.381012 });
}
private _map: google.maps.Map;
private _marker: google.maps.Marker;
public pan(coordinate: { latitude; longitude }) {
const ll: google.maps.LatLng = new google.maps.LatLng(
coordinate.latitude,
coordinate.longitude
);
// Lazily create the map and marker.
this._map =
this._map ||
new google.maps.Map(document.getElementById("iss-tracking-map"), {
center: ll,
zoom: 16
});
this._marker =
this._marker ||
new google.maps.Marker({
map: this._map,
position: ll
});
this._map.panTo(ll);
this._marker.setPosition(ll);
}
}At this point, the application shows a map, zoomed in on the beloved Ionic office.

The ISS moves pretty fast — about 27,600 km/h (17,100 mph) — so with the map zoomed in that close it won't be accurate for long!
First, edit pages/map/map.ts and change the zoom config
to 3. A zoom level of 1 is the whole world. 15 is stree-level.
For ISS, we want to show the general area on the globe, so
we're using 3.

The standard Google marker doesn't look much like the ISS. Instead, use this image.
Right click on the image, and download the linked file, then
save it to src/assets/imgs
Then change the code that creates the marker to use
the image. Edit pages/map/map.ts and use this code
where the market is created in the pan() method.
this._marker =
this._marker ||
new google.maps.Marker({
map: this._map,
position: ll,
icon: {
url: "assets/imgs/iss.png",
anchor: new google.maps.Point(41, 16),
scaledSize: new google.maps.Size(82, 33)
}
});
{
"timestamp": 1522090894,
"message": "success",
"iss_position": {
"longitude": "118.3526",
"latitude": "24.8851"
}
}
// timestamp is a UNIX timestamp (seconds)
Limit calls to about one every five seconds
Appending a &callback parameter will invoke a JSONP response
{
"message": "success",
"response": [ { "duration": 376, "risetime": 1522105763 },
{ "duration": 631, "risetime": 1522111378 },
{ "duration": 625, "risetime": 1522117174 },
{ "duration": 588, "risetime": 1522123024 },
{ "duration": 618, "risetime": 1522128843 } ]
}You can also specify &passes and &altitude (meters elevation)
Appending a &callback parameter will invoke a JSONP response
{
"message": "success",
"number": 6,
"people": [{ "name": "Anton Shkaplerov" , "craft": "ISS" },
{ "name": "Scott Tingle" , "craft": "ISS" },
{ "name": "Norishige Kanai" , "craft": "ISS" },
{ "name": "Oleg Artemyev" , "craft": "ISS" },
{ "name": "Andrew Feustel" , "craft": "ISS" },
{ "name": "Richard Arnold" , "craft": "ISS" } ]
}Appending a &callback parameter will invoke a JSONP response
<!-- Sample from https://developers.google.com/maps/documentation/javascript/adding-a-google-map -->
<!DOCTYPE html>
<html>
<head>
<title>Simple Map</title>
<meta name="viewport" content="initial-scale=1.0">
<meta charset="utf-8">
<style>
/* Always set the map height explicitly. */
#map { height: 100%; }
html, body { height: 100%; margin: 0; padding: 0; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var map;
function initMap() {
map = new google.maps.Map(
document.getElementById('map') ,
{ center: {lat: -34.397, lng: 150.644 },
zoom: 8}
);
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"async defer></script>
</body>
</html>You saw that earlier, in the Google Maps API setup — Google allows cross-original calls, or JSONP (shown here)
// In the provider
location(): Observable<Position> {
return this.http
.get('http://api.open-notify.org/iss-now.json')
.pipe(
map(res => (res as any).iss_position as Position)
);
}
// In the view
showLocation() {
this.tracking.location()
.subscribe( x => this.pan(x) );
}
In this lab you will get information about the International Space Station.
You will:
Open a terminal window and navigate to the iss directory
and enter this command.
ionic generate provider iss-tracking-dataThis creates a data provider named IssTrackingDataProvider in providers/iss-tracking-data/iss-tracking-data.ts and adds it to the providers list in src/app/app.modult.ts.
If you were to try to use this provider now, Angular would throw an exception.
StaticInjectorError[HttpClient]: NullInjectorError: No provider for HttpClient!That's because IssTrackingDataProvider depends on HttpClient and we're not importing that module anywhere.
We have several choices:
IssTrackingDataProvider and import HttpClientModule there, but this is not generally a good idea if we will have multiple data providers as they will all need HttpClientModule, and therefore, the code would create multiple instances of the HttpClient serviceHttpClientModule there and then import/export all of our providers from that module. This is a good approach.HttpClientModule to the app module — this is also a good option, and we'll use it here.Edit app.module.ts and add this import at the top.
import { HttpClientModule } from '@angular/common/http';Then add HttpClientModule to the module's imports array.
It's a good idea to formally type the data from your feeds.
You'll be using three Open Notify feeds, so you'll need three interface types.
Create the file src/models/astronaut.ts with this content.
export interface Astronaut {
craft: string;
name: string;
}Create the file src/models/pass.ts with this content.
export interface Pass {
duration: number;
risetime: number;
}Create the file src/models/position.ts with this content.
export interface Position {
latitude: number;
longitude: number;
}The provider needs three methods:
location()passes()astronauts()Try getting the data. Replace providers/iss-tracking-data/iss-tracking-data.ts with this.
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import { Astronaut } from '../../models/astronaut';
import { Pass } from '../../models/pass';
import { Position } from '../../models/position';
@Injectable()
export class IssTrackingDataProvider {
private baseUrl = 'http://api.open-notify.org';
constructor(private http: HttpClient) { }
location(): Observable<Position> {
return this.http.get(`${this.baseUrl}/iss-now.json`).pipe(
map(res => (res as any).iss_position as Position)
);
}
passes(position: Position): Observable<Array<Pass>> {
return this.http.get(`${this.baseUrl}/iss-pass.json?lat=${position.latitude}&lon=${position.longitude}`).pipe(
map(res => (res as any).response as Array<Pass>)
);
}
astronauts(): Observable<Array<Astronaut>> {
return this.http.get(`${this.baseUrl}/astros.json`).pipe(
map(res => (res as any).people as Array<Astronaut>)
);
}
}The code uses the new data types. It also uses rxjs/Observable — which differes from iTunes, where you used a promise.
Try out the code by editing pages/map/map.ts
and adding this import at the top.
import { IssTrackingDataProvider } from '../../providers/iss-tracking-data/iss-tracking-data';Then inject the provider in the constructor.
constructor(private navCtrl: NavController, private tracking:IssTrackingDataProvider) {}Then call the service in the ionViewDidEnter.
ionViewDidEnter() {
this.pan({ latitude: 43.074237, longitude: -89.381012 });
this.tracking.location().subscribe(x => console.log("location", x));
this.tracking.astronauts().subscribe(x => console.log("astronauts", x));
this.tracking
.passes({ latitude: 43.074237, longitude: -89.381012 })
.subscribe(x => console.log("passes", x));
}Look at the running app and check out the console.
The calls to location() and astronauts() are being
logged fine, but the call to passes() has CORS issues.
Normally you'd contact the developers who wrote the data service and have them set up CORS. But in this case, we can't do that.
Luckily, the Open Notify service is set up for JSONP.
To use that technique, you'll need to import another module in the app module.
Modify the import in app.module.ts.
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';Then add HttpClientJsonpModule to the imports array.
Then edit providers/iss-tracking-data/iss-tracking-data.ts
and replace the passes() function with this.
passes(position: Position): Observable<Array<Pass>> {
return this.http
.jsonp(
`${this.baseUrl}/iss-pass.json?lat=${position.latitude}&lon=${
position.longitude
}`,
"callback"
)
.pipe(map(res => (res as any).response as Array<Pass>));
}When you've saved your changes, all three calls should work, and the results logged.

Odds are, the ISS isn't over Ionic headquarters. And
now that the position feed is working, you can
update the marker.
Edit pages/map/map.ts and replace ionViewDidLoad with this code.
ionViewDidEnter() {
this.tracking.location().subscribe(x => {
this.pan(x);
});
}Now you'll see where the ISS actually is!

It would be cool to see the position more in real-time.
Javascript provides a setInterval([function],[interval]) function that will run the specified function every interval milliseconds. You'll use that to update the marker.
Edit pages/map/map.ts and add an intervalId field.
intervalId:number;Then add a method that shows the location.
showLocation() {
this.tracking.location().subscribe(x => {
this.pan(x);
});
}Then replace ionViewDidEnter with this code.
ionViewDidEnter() {
this.showLocation();
this.intervalId = setInterval(this.showLocation.bind(this), 3000);
}Finally, you don't want to keep calling the position service when the map isn't visible. Add this method to stop the interval when the user leaves.
ionViewDidLeave() {
clearInterval(this.intervalId);
}As you can see, the code calls showLocation initially,
and then every three seconds. It you look at the app,
you can slowly see the ISS wend its way around the earth.
If you look at network traffic in the Chrome debugger you can see the calls being made every three seconds, but they stop if you choose a different tab.
Lists items can contain a range of component types or plain HTML
<ion-item> elements can be hard coded
But more typically, the data is in an array
Now that the data provider is set up, let's show the list of astronauts.
Edit pages/astronauts/astronauts.ts and
add these two lifecycle methods.
ionViewDidLoad() {
console.log('astronauts ionViewDidLoad');
}
ionViewDidEnter() {
console.log('astronauts ionViewDidEnter');
} Then in the running app, click back and forth
from the astronauts tab to the passes tab. As
you do, you'll see the log messages. Note that
ionViewDidLoad is only run once, the first
time the page is visited, and ionViewDidEnter
is run every time the page is visited.
We can use either method as the place where we load the data. If you think the data won't update
very much, you could load it once, in ionViewDidLoad. If the data is volitile, you
could load it every time the page is visited, in ionViewDidEnter.
Even though the ISS crew changes more often than you might think, for this lab you'll fetch astronauts data in the ionViewDidLoad method.
See how much you can figure things out on your
own. You can look how you did these things in pages/map/map and in iTunes, and you can google
the gigabyes of Angular and Ionic API docs
available on the internet.
Edit pages/astronauts/astronauts.ts.
Here's what you need to do:
IssTrackingDataProvider inject it in the constructorAstronaut interfaceastronauts which is an array of type Astronaut
astronauts array in the ionViewDidLoad
Then edit astronauts.html.
<ion-content> with a list and item*ngFor to loop over the astronauts arrayIf you get stuck, keep working. ;-)
If you really get stuck, the ending code is given below.
Here's how your astronauts class and template may have ended up, written in nearly invisible ink.
<ion-header>
<ion-navbar>
<ion-title>
Astronauts
</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let astronaut of astronauts">
<h1>{{astronaut.name}}</h1>
</ion-item>
</ion-list>
</ion-content>import { Component } from "@angular/core";
import { NavController } from "ionic-angular";
import { Astronaut } from "../../models/astronaut";
import { IssTrackingDataProvider } from "../../providers/iss-tracking-data/iss-tracking-data";
@Component({
selector: "page-astronauts",
templateUrl: "astronauts.html"
})
export class AstronautsPage {
constructor(
public navCtrl: NavController,
private tracking: IssTrackingDataProvider
) {}
astronauts: Astronaut[];
ionViewDidLoad() {
this.tracking.astronauts().subscribe(data => (this.astronauts = data));
}
ionViewDidEnter() {
console.log("astronauts ionViewDidEnter");
}
}You're showing the astronauts. Now you need to show upcoming passes.
This is similar to what you did for Astronauts. In this
case, you'll fetch the data in the ionViewDidEnter.
Before starting you should review the Pass interface in models/pass.ts.
Here's an example of calling the passes function, using
the hard-coded location of the Ionic offices. You'll
use code like this when you fetch the data in the PassesPage#ionViewDidEnter method.
this.tracking
.passes({ latitude: 43.074237, longitude: -89.381012 })
.subscribe(x => /* Do something meaningful here */);But in your version, you'll need to use the actual location.
You're already doing that in map.ts — look at the
showLocation() function. It gets the location and in the
callback, it pans the map. In pass.ts you'll do something
similar, except rather than panning, you'll call
this.tracking.passes(), passing in the location.
Here's what you have to do to show passes:
Pass interfacepasses which is an array of type Pass
passes array in the ionViewDidEnter (following the advice in the paragraph above)passes.html
<ion-content> with a list and item*ngFor to loop over the passes arrayrisetime and y is the pass duration
Once you get the code running, you'll see that the the view is showing items that read something like 15198344386 for 621 seconds.
That's because the rise time is in the wrong format.

One of the problems is that risetime in the data
is a UNIX timestamp — the number of seconds past
some epoch start time. You need to convert that to
a Date.
First, edit models/Pass and change the risetime
property to be of type Date.
export interface Pass {
duration: number;
risetime: Date;
}Then edit the service and transform the data property as it's read.
passes(position: Position): Observable<Array<Pass>> {
return this.http.jsonp(`${this.baseUrl}/iss-pass.json?lat=${position.latitude}&lon=${position.longitude}`, 'callback').pipe(
map(res => {
const data = (res as any).response.map(r => ({
duration: r.duration,
risetime: new Date(r.risetime * 1000)
}));
return data;
})
);
}Note that the Date constructor expects a time in milliseconds,
so the code multiplies the risetime by 1000.
Now what pages/passes/passes.html and have it show
the rise time as a formatted date. To do that, use a pipe.
Here's how the item ends up looking.
<ion-item *ngFor="let pass of passes">
At {{pass.risetime | date:'H:mm, EEEE, MMMM d' }} for {{pass.duration}} seconds
</ion-item>
import { Component } from '@angular/core';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
todate = new Date();
}
<ion-content>
<h1>Today is {{ today | date }}</h1>
</ion-content><ion-content>
<h1>Today is {{ today | date:'foo':2:false }}</h1>
</ion-content><ion-content>
<h1>Today is {{ today | date | uppercase }}</h1>
</ion-content>You're showing passes, but the formatting needs improvement. In this lab you'll use custom pipes to show pass information.
Only a few upcoming passes are only shown, and they are always today, or tomorrow (if you look late in the day).
Therefore, the list items would read more naturally if they said something like Today at 2:24pm...
You could put together a string in the view's class. But it's more reusable (and more fun) to wrap that functionality up in a pipe.
Generate a new pipe by going to the command line
and navigate to the
iss directory, then enter
this command.
ionic generate pipe today-or-tomorrow
Similarly to what it does for components, Ionic
will create a module for all pipes named
pipes/pipes.module.ts. But you still need to
include that in the app module.
Edit
app/app.module.ts and import the new pipes
module, then add
PipesModule to the
imports array.
Your code for the pipe will use moment — a popular Javascript date library.
Use a terminal window and navigate to the
iss directory,
and install moment.
npm install moment
Now edit
pipes/today-or-tomorrow/today-or-tomorrow.ts and completely replace the contents
with this.
import { Pipe, PipeTransform } from "@angular/core";
import * as moment from "moment";
@Pipe({
name: "todayOrTomorrow"
})
export class TodayOrTomorrowPipe implements PipeTransform {
/**
* Based on the difference between today and the specified
* date, returns either "n days ago", "Yesterday", "Today",
* or "In n days"
*/
transform(value: Date, ...args) {
// TODO: This won't work for dates that span end of year!
let diff = this.julian(value) - this.julian(new Date());
console.log(diff);
if (diff < 0) {
return diff === -1 ? "Yesterday" : `${-diff} days ago`;
} else if (diff === 0) {
return "Today";
} else {
return diff === 1 ? "Tomorrow" : `In ${diff} days`;
}
}
private julian(date: Date) {
// Some funky algorithm that returns the number of days from
// some epoch start date. Useful for determining the day of year.
const d = date.getDate();
const y = date.getFullYear();
const m = date.getMonth() + 1;
return Math.floor(1461 * (y + 4800 + (m - 14) / 12) / 4 + 367 * (m - 2 - 12 * ((m - 14) / 12)) / 12 - 3 * ((y + 4900 + (m - 14) / 12) / 100) / 4 + d - 32075);
}
}
As you can see, the routine looks at the difference in days from the provided date and the current date. It returns a string showing how many days the date is in the past or future.
todayortomorrow pipe
Now edit
pages/passes/passes.html and
modify the body of the
<ion-item> to use
the pipe.
Can you figure out the syntax? You'll need
to use the new pipe, and modify the call to
Angular's
date pipe to use the pre-defined
shortTime format.
If you get stuck coding the
<ion-item> body,
here's the code, in nearly-invisible writing.
{{pass.risetime | todayOrTomorrow}} at {{pass.risetime | date:'shortTime' }} for {{pass.duration}} seconds
The duration needs improving too. Currently, it says something like "623 seconds", which is not that clear. It would be better to phrase that as "10 minutes 23 seconds".
Recall how you created the
today-or-tomorrow
pipe, and do it again for a pipe named
seconds-to-minutes.
You are already importing the pipes module to the app module. And when you generate a new pipe, Ionic automatically adds it to the pipes module. So there's nothing you need to do to use the new pipe.
You'll have to figure out the algorithm. Why? Because that's how programmers have fun!
Here are the key formulas:
Math.floor(seconds/60)
(seconds % 60)
Given a number number of seconds, the pipe should return a string reading like these examples.
Note how minutes and seconds may or may not be pluralized. You could probably code the routine using Chromes console.
Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
<ion-header>
<ion-navbar>
<ion-title>
Passes </ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header *ngIf="(passes && location)">
Next {{passes.length}} passes for {{location}}
</ion-list-header>
<ion-item *ngFor="let pass of passes">
{{pass.risetime | todayOrTomorrow}} at {{pass.risetime | date:'shortTime' }} for {{pass.duration | secondsToMinutes}}
</ion-item>
</ion-list>
</ion-content>
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "secondsToMinutes"
})
export class SecondsToMinutesPipe implements PipeTransform {
transform(duration: number, ...args) {
const minutes = Math.floor(duration / 60);
let s = minutes + (minutes === 1 ? " minute " : " minutes ");
const seconds = duration % 60;
s += seconds + (seconds === 1 ? " second" : " seconds");
return s;
}
}
Perhaps one of the last barriers to the human conquest of space has been removed; a space-rated espresso machine has now been delivered to the International Space Station (ISS).
* See https://www.w3.org/TR/geolocation-API/
The passes tab shows upcoming passes for a hard-coded location. But you want the passes to be for the user's actual location.
To determine location, you'll use the location API built into modern browsers.
Do you live in Stockholm, or Edinburgh or Moscow? Then you never see the ISS pass overhead. :-(
That's because the ISS only travels between 51.6° north and south latitudes. Why?
Crew and supplies for ISS are launched from the Russian launch site at Baikonur, which is at 45.6° latitude. It's cheapest to simply go straight up from the launch site, which would mean an orbit of 45.6°. But China is east of the launch site, so the Russians power their rockets to the north — to an inclination of 51.6° — in order to avoid going over Chinese territory.
Later, you'll add code to dynamically determine your latitude and longitude. In the mean-time, you'll just hard code it to the location of the Ionic office, or any other location you'd like.
If you live past the 52nd parallel, using a dynamically-determined location won't do you any good, because the ISS never passes overhead. In that case you'll have to hard-code your location to some city like Stockholm, Wisconsin or Edinburg, Texas or Moscow, Idaho.
Visit whatwebcando.today and see how well Geolocation is supported. It appears that it is supported well enough in all current browsers, so let's try using the web API directly.
You need a provider that does two things: - Return the user's current location - Return the location for a specified address
Initially, you'll code the method that returns the currwenr location.
Use a terminal window and navigate to the iss directory.
ionic generate provider locationUse a code editor to edit provider/location/location.ts
and replace the contents with this.
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Position } from "../../models/position";
@Injectable()
export class LocationProvider {
private _defaultPosition: Position = {
latitude: 43.074237,
longitude: -89.381012
};
constructor(public http: HttpClient) {
console.log("Hello LocationProvider Provider");
}
getCurrentPosition(): Promise<Position> {
if ("geolocation" in navigator) {
return new Promise(resolve => {
navigator.geolocation.getCurrentPosition(p => resolve(p.coords));
});
} else {
return Promise.resolve(this._defaultPosition);
}
}
}You may ask: If all the code is doing is call the browser's geocoder, why use a provider? The answer is to make the code more encapsulated and abstract — the class does what you need and hides how. that fact from the rest of the code. In the future you may change the code to use a Cordova plugin, or use some other technique, and the rest of the app doesn't care.
Edit pages/passes/passes.ts and review ionViewDidEnter.
Is it necessary to determine the upcoming passes every time the page is visited? Probably not. The ISS makes an orbit about every 92 minutes. Therefore, you could code the provider to cache the data, and only fetch when you're more than 92 minutes from the previously determined next pass. But we won't bother doing that.
Modify pages/passes/passes.ts and add an import for the new location provider. Then inject it into the constructor.
Then edit ionViewDidEnter and have it call the provider's
getCurrentPosition method to determine the location.
When you get the code written, and have saved your changes, look at the running app and select the Passes tab. You should see an alert from the browser asking for permission to determine your location. The browser remembers your response, so you should only have to give permission once.
If you look in the debugger's Network tab, and filter on iss-passes, you can verify that the header contains your actual location.

Note that Chrome will only determine your location when you
give permission, and you are either running on localhost or
via HTTPS. (Safari refuses completely when running
http://localhost.)
Another thing you probably noticed is that it takes a long time to get the result! That's because it takes several seconds to get the user's location. In a production app you'd probably want some strategy for caching that.
Here's the finishing code. You can peek here in case you get stuck, or want to compare your code.
import { Component } from "@angular/core";
import { NavController, LoadingController } from "ionic-angular";
import { IssTrackingDataProvider } from "../../providers/iss-tracking-data/iss-tracking-data";
import { Pass } from "../../models/pass";
import { LocationProvider } from "../../providers/location/location";
@Component({
selector: "page-passes",
templateUrl: "passes.html"
})
export class PassesPage {
public passes: Array<Pass>;
location: string;
constructor(
public loadingController: LoadingController,
public navCtrl: NavController,
public locationProvider: LocationProvider,
private tracking: IssTrackingDataProvider
) {}
ionViewDidEnter() {
this.locationProvider
.getCurrentPosition()
.then(location =>
this.tracking.passes(location).subscribe(data => (this.passes = data))
);
}
}<ion-header>
<ion-navbar>
<ion-title>
Passes </ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let pass of passes">
At {{pass.risetime | date:'H:mm, EEEE, MMMM d' }} for {{pass.duration}} seconds
</ion-item>
</ion-list>
</ion-content>You're using the provider to determine the user's location and the upcoming passes for that location.
How you can start working on showing the city.
The ISS Tracker design calls for the city name being shown at the top of the list of passes. You haven't written the code that determines the city yet, but you can stub that out.
Edit pages/passes/passes.ts and add a class variable
named location, of type string, and initialize it to
some city name, such as Madison, Wisconsin or Paris, Texas.
Then edit pages/passes/passes.html and add an <ion-list-header> within the <ion-list> that shows a sentence
reading The next 5 passes for Madison, Wisconsin. Use an
expression inside of {{...}} for the number of passes and
location. (Remember: Javascript arrays have a length property that holds the length of the array.)
Save and visit the passes tab and while you wait for the location to be determined, you should see something like this. Note that the list header reads The next 0 passes for....

ionViewDidEnter
The city should be dermined in the ionViewDidEnter, rather than being hard-coded.
Edit pages/passes/passes.ts and modify the instance fields and the ionOnDidLoad.
passes: Array<Pass>;
location: string;async ionViewDidEnter() {
const position = await this.locationProvider.getCurrentPosition();
this.location = "Madison, Wisconsin";
this.tracking.passes(position).subscribe(data => (this.passes = data));
}This code sets the location within ionViewDidEnter. The ionViewDidEnter method is also refactored to use the
cool async feature, which results in the code being less
nested and confusing. For now, the city is still hard-coded — in a later lab you'll add that functionality to
the location provider.
Also note that the two instance fields passes and
location are not initialized as they are defined. That will make it easier to conditionally show the list header.
At this point, the passes page is messed up! That's because
your code uses shows passes.length in the list header,
and initially, passes is undefined. The result is the app
throwing an error complaining that that it can't read length
of undefined.

You shouldn't be trying to show either the number of passes or the city, until those have been determined.
Edit pages/passes/passes.html and change the list
header to use an *ngIf which only shows the header if
both the location and passes hold data.
Here is an example of an ngIf from the Angular API
docs. This is not the code you need,
but an example of what you'll need.
<div *ngIf="show">Text to show</div>You need to put an ngIf on the <ion-header> with the
condition (passes && location).
Now when you visit the passes tab the first time, you
won't see the header until ionViewDidLoad finishes
its work.
Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { Component } from "@angular/core";
import { NavController, LoadingController } from "ionic-angular";
import { IssTrackingDataProvider } from "../../providers/iss-tracking-data/iss-tracking-data";
import { Pass } from "../../models/pass";
import { LocationProvider } from "../../providers/location/location";
@Component({
selector: "page-passes",
templateUrl: "passes.html"
})
export class PassesPage {
public passes: Array<Pass>;
location: string;
constructor(
public loadingController: LoadingController,
public navCtrl: NavController,
public locationProvider: LocationProvider,
private tracking: IssTrackingDataProvider
) {}
async ionViewDidEnter() {
const position = await this.locationProvider.getCurrentPosition();
this.location = "Madison, Wisconsin";
this.tracking.passes(position).subscribe(data => (this.passes = data));
}
}<ion-header>
<ion-navbar>
<ion-title>
Passes </ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header *ngIf="(passes && location)">
Next {{passes.length}} passes for {{location}}
</ion-list-header>
<ion-item *ngFor="let pass of passes">
At {{pass.risetime | date:'H:mm, EEEE, MMMM d' }} for {{pass.duration}} seconds
</ion-item>
</ion-list>
</ion-content>You're showing a hard-coded city, but Google geocoding can use the user's latitude and longitude and dynamically determine the city.
Reverse geocoding, or address lookup, is the process of using a latitude and longitude to determine an address. You need to add that to the location provider.
It's good to code in small steps, so first, create a city
reverse geocoding routing that meets the API we want.
Edit providers/location/location.ts and add this method.
city(position: Position): Promise<string> {
return new Promise(function(resolve, reject) {
resolve("Bismark, ND");
});
}
This returns a promise, but initially it just hard-codes a value.
Then edit pages/passes/passes.ts and modify the ionViewDidLoad
to use the new provider method.
async ionViewDidEnter() {
const position = await this.locationProvider.getCurrentPosition();
this.location = await this.locationProvider.city(position);
this.tracking.passes(position).subscribe(data => (this.passes = data));
}
The city method just returns a hard-coded value. You
need to actually use the provided latitude and longitude
and have Google do the reverse geolocation.
The reverse geolocation API is simple to use, but hard to interpret because a location may be in the middle of the desert, and the political breakdown of cities, municipalities, shires, cantons, counties, etc., differ from country to country.
The API returns several addresses for a given location, ordered from more specific to less specific.
The key address type we need is a locality, but that can be buried in the data. Take a look. Due to the magic of Javascript, you can often try code out right in the debugger. (This works for Javascript, not TypeScript.)
In the broswer window running your app, open the console, and copy and paste this code.
geocoder = new google.maps.Geocoder();
function city(latitude, longitude){
latLng = new google.maps.LatLng(latitude, longitude);
geocoder.geocode({ location: latLng }, (results, status) => {
console.log(results);
});
}
Try looking at the data for downtown New York.
city(40.7104558,-74.0135321);There are a lot of results. Each item has a formattedAddress, type[] and address_components[]. And each address_component item has its own type[]. Look for locality in one of the type arrays.
There are two places a locality can be found: at the
top level (immediate children of the results array),
or within on of the child address_components. The
most general top-level local seems to return the most
natural address. If there isn't a top-level locality,
then just using the locality found within result[0]
is probably ok.
Try a few more to test that theory.
The last one didn't coincide with a city, so there's no locality in there at all!
The city routine will need the Google geocoder.
Edit providers/location/location.js and create
an instance field.
private geocoder = new google.maps.Geocoder();Then replace the city function with this version the
intern came up with.
city(position: Position): Promise<string> {
const latLng = new google.maps.LatLng(
position.latitude,
position.longitude
);
return new Promise((resolve, reject) => {
this.geocoder.geocode({ location: latLng }, (results, status) => {
if ("OK" === status.toString() && results[0]) {
console.log(results);
// The most general top-level locality has the most natural
// formatted address. Addresses are in most specific to
// most general order, so loop backwards.
for (let i = results.length - 1; i >= 0; i--) {
let r = results[i];
if (r.types.indexOf("locality") > -1) {
resolve(r.formatted_address);
return; // Bail out
}
}
// Assert: We didn't find a top level locality.
// Return the long name associated with the first locality,
// or if not found, use the formatted_address.
let addresses = results[0].address_components;
let result = results[0].formatted_address; // Used if no locality found
for (let i = 0; i < addresses.length; i++) {
let address = addresses[i];
if (address.types.indexOf("locality") > -1) {
resolve(address.long_name);
return; // Bail out
}
}
// Assert: We didn't find a locality at all! Use the
// most specific address's formatted address.
resolve(results[0].formatted_address);
} else {
resolve("unknown location");
}
});
});
}
At this point, your app no longer shows the hard-coded value, but the actual city name for your location!

Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { Component } from "@angular/core";
import { NavController, LoadingController } from "ionic-angular";
import { IssTrackingDataProvider } from "../../providers/iss-tracking-data/iss-tracking-data";
import { Pass } from "../../models/pass";
import { LocationProvider } from "../../providers/location/location";
@Component({
selector: "page-passes",
templateUrl: "passes.html"
})
export class PassesPage {
public passes: Array<Pass>;
location: string;
constructor(
public loadingController: LoadingController,
public navCtrl: NavController,
public locationProvider: LocationProvider,
private tracking: IssTrackingDataProvider
) {}
async ionViewDidEnter() {
const position = await this.locationProvider.getCurrentPosition();
this.location = await this.locationProvider.city(position);
this.tracking.passes(position).subscribe(data => (this.passes = data));
}
}
<ion-header>
<ion-navbar>
<ion-title>
Passes </ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header *ngIf="(passes && location)">
Next {{passes.length}} passes for {{location}}
</ion-list-header>
<ion-item *ngFor="let pass of passes">
At {{pass.risetime | date:'H:mm, EEEE, MMMM d' }} for {{pass.duration}} seconds
</ion-item>
</ion-list>
</ion-content>
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Position } from "../../models/position";
@Injectable()
export class LocationProvider {
private _defaultPosition: Position = {
latitude: 43.074237,
longitude: -89.381012
};
constructor(public http: HttpClient) {
console.log("Hello LocationProvider Provider");
}
getCurrentPosition(): Promise<Position> {
if ("geolocation" in navigator) {
return new Promise(resolve => {
navigator.geolocation.getCurrentPosition(p => resolve(p.coords));
});
} else {
return Promise.resolve(this._defaultPosition);
}
}
private geocoder = new google.maps.Geocoder();
city(position: Position): Promise<string> {
const latLng = new google.maps.LatLng(
position.latitude,
position.longitude
);
return new Promise((resolve, reject) => {
this.geocoder.geocode({ location: latLng }, (results, status) => {
if ("OK" === status.toString() && results[0]) {
console.log(results);
// The most general top-level locality has the most natural
// formatted address. Addresses are in most specific to
// most general order, so loop backwards.
for (let i = results.length - 1; i >= 0; i--) {
let r = results[i];
if (r.types.indexOf("locality") > -1) {
resolve(r.formatted_address);
return; // Bail out
}
}
// Assert: We didn't find a top level locality.
// Return the long name associated with the first locality,
// or if not found, use the formatted_address.
let addresses = results[0].address_components;
let result = results[0].formatted_address; // Used if no locality found
for (let i = 0; i < addresses.length; i++) {
let address = addresses[i];
if (address.types.indexOf("locality") > -1) {
resolve(address.long_name);
return; // Bail out
}
}
// Assert: We didn't find a locality at all! Use the
// most specific address's formatted address.
resolve(results[0].formatted_address);
} else {
resolve("unknown location");
}
});
});
}
}
You're showing the user's city, but there's still a long wait after going to passes tab. That's because it takes several seconds to determine the user's location.
Rather than just show an empty page, you should show a load message while the location is being determined.
The long pause when you first choose the passes page makes it look like something is wrong. You should't have the user guessing if something is going on. In this case you should show some kind of loading indicator so the user knows what's happening. Once you get the data, you'll remove the loading indicator.
Edit
pages/passes/passes.ts and import
LoadingController
(which is part of
ionic-angular), then inject it into
the constructor.
Then modify the
ionViewDidEnter.
async ionViewDidEnter() {
const loading = this.loadingController.create({
spinner: "crescent",
content: "Determining location and loading passes."
});
loading.present();
const position = await this.locationProvider.getCurrentPosition();
this.location = await this.locationProvider.city(position);
this.tracking.passes(position).subscribe(data => (this.passes = data));
loading.dismiss();
}
Try it out! When you select Passes, you should see the new load message while the location and passes are being determined.
There's a little bug in the
ionViewDidEnter. Do you see it?
Look closely at the
ionViewDidEnter method. The loading
indicator is dismissed
before we get the result from
this.tracking.passes. That call is pretty quick, so the
user probably may not notice, but it's still not correct.
To fix that, you move the
loading.dismiss() to the arrow function that sets
this.passes.
See if you can code it. Remember: if you have an arrow
function that has more than one statement, it has to
be placed in a function body using curly braces. (If you
were returning a result, you'd have to explictly code
a
return too, but you don't need that in this case.)
Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { Component } from "@angular/core";
import { NavController, LoadingController } from "ionic-angular";
import { IssTrackingDataProvider } from "../../providers/iss-tracking-data/iss-tracking-data";
import { Pass } from "../../models/pass";
import { LocationProvider } from "../../providers/location/location";
@Component({
selector: "page-passes",
templateUrl: "passes.html"
})
export class PassesPage {
public passes: Array<Pass>;
location: string;
constructor(
public loadingController: LoadingController,
public navCtrl: NavController,
public locationProvider: LocationProvider,
private tracking: IssTrackingDataProvider
) {}
async ionViewDidEnter() {
const loading = this.loadingController.create({
spinner: "crescent",
content: "Determining location and loading passes."
});
loading.present();
const position = await this.locationProvider.getCurrentPosition();
this.location = await this.locationProvider.city(position);
this.tracking.passes(position).subscribe(data => {
this.passes = data;
loading.dismiss();
});
}
}
In this lab you'll generate the configurartion page, and add it as a tab.
Open a terminal window, navigate to the
iss directory
and run this command.
ionic generate page configuration
The generator assumes you're using lazy loading, which you
are not. Therefore, you need to manually add the page to your
app.modules.ts file.
You have a couple of options:
Import the page
module, then add it to the app module's
imports array
import { ConfigurationPageModule } from "./../pages/configuration/configuration.module";
...
imports: [
ConfigurationPageModule,
...
Import the
page, then add it to the app module's
import and
entryComponents arrays.
import { ConfigurationPage } from "./../pages/configuration/configuration";
...
declarations: [
ConfigurationPage,
...
entryComponents: [
ConfigurationPage,
...
Neither alternative is preferred, so choose whichever you'd like.
Now that the configuration page exists, you need to add it as the fourth tab.
Edit
pages/tabs/tabs.ts, import the new configuration page,
then add a new variable named
tab4, initialized to
ConfiguationPage.
Then edit
pages/tabs/tabs.html and configure a fourth tab,
whose root is
tab4Root. Set the tab icon to
options.
When you're finished, select the new tab. It's empty, but when it was generated, the title was set to the name of the page.
Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { NgModule, ErrorHandler } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { IonicApp, IonicModule, IonicErrorHandler } from "ionic-angular";
import { MyApp } from "./app.component";
import { PassesPage } from "../pages/passes/passes";
import { AstronautsPage } from "../pages/astronauts/astronauts";
import { MapPage } from "../pages/map/map";
import { TabsPage } from "../pages/tabs/tabs";
import { StatusBar } from "@ionic-native/status-bar";
import { SplashScreen } from "@ionic-native/splash-screen";
import { IssTrackingDataProvider } from "../providers/iss-tracking-data/iss-tracking-data";
import { LocationProvider } from "../providers/location/location";
import { HttpClientModule, HttpClientJsonpModule } from "@angular/common/http";
import { ConfigurationPageModule } from "../pages/configuration/configuration.module";
@NgModule({
declarations: [MyApp, PassesPage, AstronautsPage, MapPage, TabsPage],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
HttpClientModule,
ConfigurationPageModule,
HttpClientJsonpModule
],
bootstrap: [IonicApp],
entryComponents: [MyApp, PassesPage, AstronautsPage, MapPage, TabsPage],
providers: [
StatusBar,
SplashScreen,
{ provide: ErrorHandler, useClass: IonicErrorHandler },
IssTrackingDataProvider,
LocationProvider
]
})
export class AppModule {}
import { Component } from "@angular/core";
import { PassesPage } from "../passes/passes";
import { AstronautsPage } from "../astronauts/astronauts";
import { MapPage } from "../map/map";
import { ConfigurationPage } from "../configuration/configuration";
@Component({
templateUrl: "tabs.html"
})
export class TabsPage {
tab1Root = MapPage;
tab2Root = PassesPage;
tab3Root = AstronautsPage;
tab4Root = ConfigurationPage;
constructor() {}
}
<ion-tabs>
<ion-tab [root]="tab1Root" tabTitle="Map" tabIcon="locate"></ion-tab>
<ion-tab [root]="tab2Root" tabTitle="Passes" tabIcon="list"></ion-tab>
<ion-tab [root]="tab3Root" tabTitle="Astronauts" tabIcon="people"></ion-tab>
<ion-tab [root]="tab4Root" tabTitle="Configuration" tabIcon="options"></ion-tab>
</ion-tabs>
<ion-input type="text">
<ion-input type="tel">
<ion-input type="number">
<ion-input type="email">
<ion-input type="url">
<ion-input type="password">
<ion-list radio-group [(ngModel)]="relationship">
<ion-item>
<ion-label>Friends</ion-label>
<ion-radio value="friends" checked></ion-radio>
</ion-item>
<ion-item>
<ion-label>Family</ion-label>
<ion-radio value="family"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Enemies</ion-label>
<ion-radio value="enemies" [disabled]="isDisabled"></ion-radio>
</ion-item>
</ion-list>
<ion-range >
<ion-icon small range-left name="sunny"></ion-icon>
<ion-icon range-right name="sunny">
</ion-icon> </ion-range>
In this lab you'll add add the input fields to the configuration page.
You need a fourth tab, used by the user to specify a refresh rate, a city and whether or not to use the current location.
The configuration page needs three fields:
Edit
pages/configuration/configuration.html and use this
code.
<ion-header>
<ion-navbar>
<ion-title>Configuration</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label floating>Map refresh rate (seconds)</ion-label>
<ion-input type="number" min="1" max="20"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>City</ion-label>
<ion-textarea></ion-textarea>
</ion-item>
<ion-item>
<ion-label>Use current location</ion-label>
<ion-toggle></ion-toggle>
</ion-item>
</ion-content>
The data is pretty simple, so we won't bother with form validation.
At this point there's nothing linking the input
fields with the class. As you may know, that's done
via Angular's
[(ngModel)] directive, which binds
an input field with a class property.
Edit
pages/configuration/configuration.ts and add
three class fields:
refreshRate:number;
city:string;
useCurrentLocation:boolean;
Then modify
configuration.html to use
[(ngModel)]
to bind to those fields.
<ion-header>
<ion-navbar>
<ion-title>Configuration</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label floating>Map refresh rate (seconds)</ion-label>
<ion-input [(ngModel)]="refreshRate" type="number" min="1" max="20"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>City</ion-label>
<ion-input [(ngModel)]="city" type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Use current location</ion-label>
<ion-toggle [(ngModel)]="useCurrentLocation"></ion-toggle>
</ion-item>
</ion-content>
How will you know that's working? Add an
ionViewDidLeave
to
configuration.ts that logs the values as you leave the
page.
ionViewDidLeave() {
console.log("--------");
console.log(this.city);
console.log(this.refreshRate);
console.log(this.useCurrentLocation);
}
Try it out by entering some values, then tabbing off and verifying that your values are logged.
Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { Component } from "@angular/core";
import { IonicPage, NavController, NavParams } from "ionic-angular";
@IonicPage()
@Component({
selector: "page-configuration",
templateUrl: "configuration.html"
})
export class ConfigurationPage {
refreshRate: number;
city: string;
useCurrentLocation: boolean;
constructor(public navCtrl: NavController, public navParams: NavParams) {}
ionViewDidLoad() {
console.log("ionViewDidLoad ConfigurationPage");
}
ionViewDidLeave() {
console.log("--------");
console.log(this.city);
console.log(this.refreshRate);
console.log(this.useCurrentLocation);
}
}
<ion-header>
<ion-navbar>
<ion-title>Configuration</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label floating>Map refresh rate (seconds)</ion-label>
<ion-input [(ngModel)]="refreshRate" type="number" min="1" max="20"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>City</ion-label>
<ion-input [(ngModel)]="city" type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Use current location</ion-label>
<ion-toggle [(ngModel)]="useCurrentLocation"></ion-toggle>
</ion-item>
</ion-content>
this.storage.set('key', 1);
this.storage.set('key', 'hi');
this.storage.set('key', {foo: 1});
this.storage.get('key')
.then(value => console.log(value));
In this lab you'll add a provider that saves the configuration data. The provider will use the Ionic storage class.
Use a terminal window to navigate to the iss directory,
and enter this command.
npm install @ionic/storage
Ionic Storage stores data in the best storage engine available on the runtime platform: SQLite, IndexedDB, WebSQL, or LocalStorage. Of these, SQLLite is the only one that is guarenteed to not be clobbered by the mobile OS if space is tight on the host device. However, it requires extra setup which we will skip for this lab.
Edit app.module.ts and do two things.
import { IonicStorageModule } from '@ionic/storage';IonicStorageModule.forRoot(), to the imports arrayYou have the configuration page, and the storage module. Now all you need is the provider that holds and persists the values.
Use a terminal window to navigate to the iss directory,
and enter this command.
ionic generate provider configuration
Then edit providers/configuration/configuration.ts and
use this code.
import { Injectable } from "@angular/core";
import { Platform } from "ionic-angular";
import { Storage } from "@ionic/storage";
import { Position } from "../../models/position";
@Injectable()
export class ConfigurationProvider {
private _city: string;
private _refreshRate: number;
private _useCurrentLocation: boolean;
private _cityKey = "city";
private _refreshRateKey = "refreshRate";
private _useCurrentLocationKey = "useCurrentLocation";
private _initialized: boolean = false;
constructor(private platform: Platform, private storage: Storage) {}
// The user must call init one time, before calling any getter.
init(): Promise<void> {
if (this._initialized) {
return new Promise(function(resolve) {
resolve();
});
} else {
return this.loadData();
}
}
private checkInit() {
if (!this._initialized) {
console.error("Getter called before init()!");
}
}
set city(value: string) {
this.checkInit();
this._city = value;
this.store(this._cityKey, value);
}
get city(): string {
this.checkInit();
return this._city;
}
set refreshRate(value: number) {
this._refreshRate = value;
this.store(this._refreshRateKey, value);
}
get refreshRate(): number {
this.checkInit();
return this._refreshRate || 5;
}
set useCurrentLocation(value: boolean) {
this._useCurrentLocation = value;
this.store(this._useCurrentLocationKey, value);
}
get useCurrentLocation(): boolean {
this.checkInit();
return !!this._useCurrentLocation;
}
private async loadData(): Promise<void> {
await this.platform.ready();
await this.storage.ready();
await Promise.all([
(this._city = await this.storage.get(this._cityKey)),
(this._refreshRate = await this.storage.get(this._refreshRateKey)),
(this._useCurrentLocation = await this.storage.get(
this._useCurrentLocationKey
))
]);
this._initialized = true;
}
private async store(key: string, value: any) {
this.storage.set(key, value && value.toString());
}
}
The implementation details don't matter, but make note of the API:
ConfigurationProvider#refreshRate:numberConfigurationProvider#city:stringConfigurationProvider#useCurrentAddress:booleaninit method is run firstThe configuration provider is written in such a way that
the data has to be read into memory before any of its
getters is run. The initialization method is the async
method init, which returns a promise.
You could code things such that all code that includes
the configuation provider does a call to init at the
start of the page lifecycle. But that's an error-prone
approach, because some programmer maintaining the app
may forget that call.
So how do you make sure the init async call is run
before any other page is processed?
The answer is the ionViewCanEnter lifecycle method.
It's written to allow you to have it return a promise.
If you return a promise, it must be resolved before
the page is entered. You simply need to do that in the
first page loaded, which is tabs.ts.
Edit pages/tabs/tabs.ts and import the configuration
provider. Then inject it into the constructor using a
parameter named public configuration:ConfigurationProvider.
Then add this method.
ionViewCanEnter() {
console.log("TabsPage#onViewDidLoad");
return this.configuration.init();
}
The result is that the configuration provider's init
method is run before any of the individual tabs is
entered.
Now you can have the configuration page use the new provider.
Edit pages/configuration/configuration.ts and import
the new provider.
Then inject the provider in the constructor
using the param public configuration: ConfigurationProvider.
Then add an ionPageDidEnter that initializes
values using the provider.
ionViewDidEnter() {
this.city = this.configuration.city;
this.refreshRate = this.configuration.refreshRate;
this.useCurrentLocation = this.configuration.useCurrentLocation;
}
You also need to replace your ionViewDidLeave with
code that updates the configuration provider.
ionViewDidLeave() {
this.configuration.refreshRate = this.refreshRate;
this.configuration.useCurrentLocation = this.useCurrentLocation;
this.configuration.city = this.city;
}
In Chrome, try entering some values, and tab off the configuration page. Then refresh the browser window. The values you previously entered should be shown on the configuration page.
You can see then being stored too by looking at
the Application tab in the Chrome debugger. Then
choosing IndexedDB > _ionicstorage > _ionickv. As you
can see, Ionic Storage choose IndexDB to store the
values.

Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { Injectable } from "@angular/core";
import { Platform } from "ionic-angular";
import { Storage } from "@ionic/storage";
import { Position } from "../../models/position";
@Injectable()
export class ConfigurationProvider {
private _city: string;
private _refreshRate: number;
private _useCurrentLocation: boolean;
private _cityKey = "city";
private _refreshRateKey = "refreshRate";
private _useCurrentLocationKey = "useCurrentLocation";
private _initialized: boolean = false;
constructor(private platform: Platform, private storage: Storage) {}
// The user must call init one time, before calling any getter.
init(): Promise<void> {
if (this._initialized) {
return new Promise(function(resolve) {
resolve();
});
} else {
return this.loadData();
}
}
private checkInit() {
if (!this._initialized) {
console.error("Getter called before init()!");
}
}
set city(value: string) {
this.checkInit();
this._city = value;
this.store(this._cityKey, value);
}
get city(): string {
this.checkInit();
return this._city;
}
set refreshRate(value: number) {
this._refreshRate = value;
this.store(this._refreshRateKey, value);
}
get refreshRate(): number {
this.checkInit();
return this._refreshRate || 5;
}
set useCurrentLocation(value: boolean) {
this._useCurrentLocation = value;
this.store(this._useCurrentLocationKey, value);
}
get useCurrentLocation(): boolean {
this.checkInit();
return !!this._useCurrentLocation;
}
private async loadData(): Promise<void> {
await this.platform.ready();
await this.storage.ready();
await Promise.all([
(this._city = await this.storage.get(this._cityKey)),
(this._refreshRate = await this.storage.get(this._refreshRateKey)),
(this._useCurrentLocation = await this.storage.get(
this._useCurrentLocationKey
))
]);
this._initialized = true;
}
private async store(key: string, value: any) {
this.storage.set(key, value && value.toString());
}
}
import { Component } from "@angular/core";
import { PassesPage } from "../passes/passes";
import { AstronautsPage } from "../astronauts/astronauts";
import { MapPage } from "../map/map";
import { ConfigurationPage } from "../configuration/configuration";
import { ConfigurationProvider } from "../../providers/configuration/configuration";
@Component({
templateUrl: "tabs.html"
})
export class TabsPage {
tab1Root = MapPage;
tab2Root = PassesPage;
tab3Root = AstronautsPage;
tab4Root = ConfigurationPage;
constructor(public configuration: ConfigurationProvider) {}
ionViewCanEnter() {
console.log("TabsPage#onViewDidLoad");
return this.configuration.init();
}
}
import { Component } from "@angular/core";
import { PassesPage } from "../passes/passes";
import { AstronautsPage } from "../astronauts/astronauts";
import { MapPage } from "../map/map";
import { ConfigurationPage } from "../configuration/configuration";
import { ConfigurationProvider } from "../../providers/configuration/configuration";
@Component({
templateUrl: "tabs.html"
})
export class TabsPage {
tab1Root = MapPage;
tab2Root = PassesPage;
tab3Root = AstronautsPage;
tab4Root = ConfigurationPage;
constructor(public configuration: ConfigurationProvider) {}
ionViewCanEnter() {
console.log("TabsPage#onViewDidLoad");
return this.configuration.init();
}
}
The configuration page is coded and you're saving the data using Ionic Serve.
Now you need to use that in the application:
Edit
pages/map/map.ts and import the configuration
provider. Then inject it in the constructor using
the parameter
private configuration:ConfigurationProvider.
Then replace
ionViewDidEnter with this code.
ionViewDidEnter() {
console.log("MapPage#ionViewDidEnter");
this.showLocation();
const refreshRate = this.configuration.refreshRate;
console.log("refreshRate = " + refreshRate);
this.intervalId = setInterval(
this.showLocation.bind(this),
refreshRate * 1000
);
}
Save your changes, and in your web browser look at the running app. You should see the map update using whatever interval you specified on the configuration page.
It turns out, you don't have to to change the passes page at all! Instead, you need to modify the location provider to provide the user's position based on the configuration. The location provider will return either the user's actual location, or a location based on the configured city.
Edit
providers/location/location.ts and import the
configuration provider, then inject it into the constructor
using the parameter
private configuration:ConfigurationProvider.
The location provider needs a method to translate city to position. Add the following code to the class. Notice that the code defines two private properties, and a private method.
private cachedPositions = {};
private position(address: string): Promise<Position> {
return new Promise(resolve => {
if (this.cachedPositions[address]) {
resolve(this.cachedPositions[address]);
} else {
this.geocoder.geocode({ address: address }, (results, status) => {
if (status.toString() === "OK" && results[0]) {
const position = {
latitude: results[0].geometry.location.lat(),
longitude: results[0].geometry.location.lng()
};
this.cachedPositions[address] = position;
resolve(position);
} else {
resolve(this._defaultPosition);
}
});
}
});
}
As you can see, the
position method
returns the position of the specified address. It also
caches addresses in memory, so if it's called more than
once for a given address, it uses the previously-calculated
value.
Now you have to modify the
getUserPosition method to
either use the new
position method when a city is
configured, or use the user's actual position.
Replace the
getUserPosition method with this code.
getUserPosition(): Promise<Position> {
if (this.configuration.useCurrentLocation || !this.configuration.city) {
if ("geolocation" in navigator) {
return new Promise(resolve => {
navigator.geolocation.getCurrentPosition(p => resolve(p.coords));
});
} else {
return Promise.resolve(this._defaultPosition);
}
} else {
return this.position(this.configuration.city);
}
}
As you can see, it checks whether the current location
should be used. If so, it does what it used to do.
If not, it returns the position of the configured city.
Recall that the
position() method caches the city,
so you can call it multiple times as the user visits the
passes page, and it will only calculate the city's position
the first time in.
Try things out. Go to the configuration tab, and set turn off use current position and choose Balikpapan City, Malasia.
Then in the running app, look at network traffic and filter on iss-pass and you should see that Borneo's latitude and longitude are being used, a location around latitude -1.1, longitude 116.7.
Here's the finishing code, provided in case you get stuck, or if you'd like to compare your code.
import { ConfigurationProvider } from "./../../../src-save/providers/configuration/configuration";
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Position } from "../../models/position";
@Injectable()
export class LocationProvider {
private _defaultPosition: Position = {
latitude: 43.074237,
longitude: -89.381012
};
constructor(
private configuration: ConfigurationProvider,
public http: HttpClient
) {
console.log("Hello LocationProvider Provider");
}
getUserPosition(): Promise<Position> {
if (this.configuration.useCurrentLocation || !this.configuration.city) {
if ("geolocation" in navigator) {
return new Promise(resolve => {
navigator.geolocation.getCurrentPosition(p => resolve(p.coords));
});
} else {
return Promise.resolve(this._defaultPosition);
}
} else {
return this.position(this.configuration.city);
}
}
private geocoder = new google.maps.Geocoder();
private cachedPositions = {};
private position(address: string): Promise<Position> {
return new Promise(resolve => {
if (this.cachedPositions[address]) {
resolve(this.cachedPositions[address]);
} else {
this.geocoder.geocode({ address: address }, (results, status) => {
if (status.toString() === "OK" && results[0]) {
const position = {
latitude: results[0].geometry.location.lat(),
longitude: results[0].geometry.location.lng()
};
this.cachedPositions[address] = position;
resolve(position);
} else {
resolve(this._defaultPosition);
}
});
}
});
}
city(position: Position): Promise<string> {
const latLng = new google.maps.LatLng(
position.latitude,
position.longitude
);
return new Promise((resolve, reject) => {
this.geocoder.geocode({ location: latLng }, (results, status) => {
if ("OK" === status.toString() && results[0]) {
console.log(results);
// The most general top-level locality has the most natural
// formatted address. Addresses are in most specific to
// most general order, so loop backwards.
for (let i = results.length - 1; i >= 0; i--) {
let r = results[i];
if (r.types.indexOf("locality") > -1) {
resolve(r.formatted_address);
return; // Bail out
}
}
// Assert: We didn't find a top level locality.
// Return the long name associated with the first locality,
// or if not found, use the formatted_address.
let addresses = results[0].address_components;
let result = results[0].formatted_address; // Used if no locality found
for (let i = 0; i < addresses.length; i++) {
let address = addresses[i];
if (address.types.indexOf("locality") > -1) {
resolve(address.long_name);
return; // Bail out
}
}
// Assert: We didn't find a locality at all! Use the
// most specific address's formatted address.
resolve(results[0].formatted_address);
} else {
resolve("unknown location");
}
});
});
}
}
import { Component } from "@angular/core";
import { NavController } from "ionic-angular";
import { IssTrackingDataProvider } from "../../providers/iss-tracking-data/iss-tracking-data";
import { ConfigurationProvider } from "../../providers/configuration/configuration";
@Component({
selector: "page-map",
templateUrl: "map.html"
})
export class MapPage {
private intervalId: number;
constructor(
private configuration: ConfigurationProvider,
private navCtrl: NavController,
private tracking: IssTrackingDataProvider
) {}
ionViewDidEnter() {
console.log("MapPage#ionViewDidEnter");
this.showLocation();
const refreshRate = this.configuration.refreshRate;
console.log("refreshRate = " + refreshRate);
this.intervalId = setInterval(
this.showLocation.bind(this),
refreshRate * 1000
);
}
showLocation() {
this.tracking.location().subscribe(x => {
this.pan(x);
});
}
private _map: google.maps.Map;
private _marker: google.maps.Marker;
public pan(coordinate: { latitude; longitude }) {
const ll: google.maps.LatLng = new google.maps.LatLng(
coordinate.latitude,
coordinate.longitude
);
// Lazily create the map and marker.
this._map =
this._map ||
new google.maps.Map(document.getElementById("iss-tracking-map"), {
center: ll,
zoom: 3
});
this._marker =
this._marker ||
new google.maps.Marker({
map: this._map,
position: ll,
icon: {
url: "assets/imgs/iss.png",
scaledSize: new google.maps.Size(82, 33)
}
});
this._map.panTo(ll);
this._marker.setPosition(ll);
}
}