瀏覽代碼

Remove unnecessary code listings

Matt Raible 7 年之前
父節點
當前提交
60fe89870b
共有 1 個檔案被更改,包括 38 行新增688 行删除
  1. 38
    688
      DEMO.md

+ 38
- 688
DEMO.md 查看文件

1
 # Spring Boot, Ionic, and Stormpath
1
 # Spring Boot, Ionic, and Stormpath
2
 
2
 
3
-This demo script shows Matt's IntelliJ Live Template shortcuts to build an Ionic and Spring Boot app.
4
-
5
-**Prerequisites**: Java 8, Node.js, Maven, a [Stormpath Account](https://api.stormpath.com/register), and an `apiKey.properties` file in `~/stormpath/`.
3
+This demo script shows pre-recorded IntelliJ Live Template shortcuts to build an Ionic and Spring Boot app. **Prerequisites**: Java 8, Node.js, Maven, a [Stormpath Account](https://api.stormpath.com/register), and an `apiKey.properties` file in `~/stormpath/`.
6
 
4
 
7
 ## Spring Boot API
5
 ## Spring Boot API
8
 
6
 
13
 dependencies==data-jpa,data-rest,h2,web,devtools,security,stormpath -d
11
 dependencies==data-jpa,data-rest,h2,web,devtools,security,stormpath -d
14
 ```
12
 ```
15
 
13
 
16
-Run the application with `./mvnw spring-boot:run`. Create a `Beer` entity class in `src/main/java/com/example/beer`. 
17
-
18
-→ **boot-entity**
19
-
20
-```java
21
-package com.example.beer;
22
-
23
-import javax.persistence.Entity;
24
-import javax.persistence.GeneratedValue;
25
-import javax.persistence.Id;
26
-
27
-@Entity
28
-public class Beer {
29
-
30
-    @Id
31
-    @GeneratedValue
32
-    private Long id;
33
-    private String name;
34
-
35
-    public Beer() {
36
-    }
37
-
38
-    public Beer(String name) {
39
-        this.name = name;
40
-    }
41
-
42
-    public Long getId() {
43
-        return id;
44
-    }
45
-
46
-    public void setId(Long id) {
47
-        this.id = id;
48
-    }
49
-
50
-    public String getName() {
51
-        return name;
52
-    }
53
-
54
-    public void setName(String name) {
55
-        this.name = name;
56
-    }
57
-
58
-    @Override
59
-    public String toString() {
60
-        return "Beer{" +
61
-                "id=" + id +
62
-                ", name='" + name + '\'' +
63
-                '}';
64
-    }
65
-}
66
-```
67
-
68
-Create a JPA Repository to manage the `Beer` entity.
69
-
70
-→ **boot-repository**
71
-
72
-```java
73
-package com.example.beer;
74
-
75
-import org.springframework.data.jpa.repository.JpaRepository;
76
-import org.springframework.data.rest.core.annotation.RepositoryRestResource;
77
-
78
-@RepositoryRestResource
79
-interface BeerRepository extends JpaRepository<Beer, Long> {
80
-}
81
-```
82
-
83
-Create a CommandLineRunner to populate the database.
84
-
85
-→ **boot-command**
86
-
87
-```java
88
-package com.example.beer;
89
-
90
-import org.springframework.boot.CommandLineRunner;
91
-import org.springframework.stereotype.Component;
92
-
93
-import java.util.stream.Stream;
94
-
95
-@Component
96
-class BeerCommandLineRunner implements CommandLineRunner {
97
-    private final BeerRepository repository;
98
-
99
-    public BeerCommandLineRunner(BeerRepository repository) {
100
-        this.repository = repository;
101
-    }
102
-
103
-    @Override
104
-    public void run(String... strings) throws Exception {
105
-        System.out.println(repository.findAll());
106
-    }
107
-}
108
-```
109
-
110
-Add default data in the `run()` method:
111
-
112
-→ **boot-add**
113
-
114
-```java
115
-// top 5 beers from https://www.beeradvocate.com/lists/top/
116
-Stream.of("Good Morning", "Kentucky Brunch Brand Stout", "ManBearPig", "King Julius",
117
-        "Very Hazy", "Budweiser", "Coors Light", "PBR").forEach(name ->
118
-        repository.save(new Beer(name))
119
-);
120
-```
121
-
122
-Create a `BeerController` for your REST API. Add some business logic that results in a `/good-beers` endpoint.
123
-
124
-→ **boot-controller**
125
-
126
-```java
127
-package com.example.beer;
128
-
129
-import org.springframework.web.bind.annotation.GetMapping;
130
-import org.springframework.web.bind.annotation.RestController;
131
-
132
-import java.util.Collection;
133
-import java.util.HashMap;
134
-import java.util.Map;
135
-import java.util.stream.Collectors;
136
-
137
-@RestController
138
-public class BeerController {
139
-    private BeerRepository repository;
140
-
141
-    public BeerController(BeerRepository repository) {
142
-        this.repository = repository;
143
-    }
144
-    
145
-    @GetMapping("/list-beers")
146
-    public Collection<Beer> list() {
147
-        return repository.findAll();
148
-    }
149
-}
150
-```
151
-
152
-Add a `/get-beers` mapping that filters out beers that aren't great.
153
-
154
-→ **boot-good**
155
-
156
-```java
157
-@GetMapping("/good-beers")
158
-public Collection<Map<String, String>> goodBeers() {
159
-
160
-    return repository.findAll().stream()
161
-            .filter(this::isGreat)
162
-            .map(b -> {
163
-                Map<String, String> m = new HashMap<>();
164
-                m.put("id", b.getId().toString());
165
-                m.put("name", b.getName());
166
-                return m;
167
-            }).collect(Collectors.toList());
168
-}
169
-
170
-private boolean isGreat(Beer beer) {
171
-    return !beer.getName().equals("Budweiser") &&
172
-            !beer.getName().equals("Coors Light") &&
173
-            !beer.getName().equals("PBR");
174
-}
175
-```
14
+1. Run the application with `./mvnw spring-boot:run`. Create a `Beer` entity class in `src/main/java/com/example/beer`. → **boot-entity**
15
+2. Create a JPA Repository to manage the `Beer` entity (tip: `@RepositoryRestResource`). → **boot-repository**
16
+3. Create a CommandLineRunner to populate the database. → **boot-command**
17
+4. Add default data in the `run()` method. → **boot-add**
18
+5. Create a `BeerController` for your REST API. Add some business logic that results in a `/good-beers` endpoint. → **boot-controller**
19
+6. Add a `/get-beers` mapping that filters out beers that aren't great. → **boot-good**
176
 
20
 
177
 Access the API using `http localhost:8080/good-beers --auth <user>:<password>`.
21
 Access the API using `http localhost:8080/good-beers --auth <user>:<password>`.
178
 
22
 
188
 
32
 
189
 ```
33
 ```
190
 ionic start ionic-beer --v2
34
 ionic start ionic-beer --v2
191
-```
192
-
193
-This may take a minute or two to complete.
194
-```
195
 cd ionic-beer
35
 cd ionic-beer
196
 ionic serve
36
 ionic serve
197
 ```
37
 ```
200
 
40
 
201
 ```json
41
 ```json
202
 "dependencies": {
42
 "dependencies": {
203
-  "@angular/common": "2.3.1",
204
-  "@angular/compiler": "2.3.1",
205
-  "@angular/compiler-cli": "2.3.1",
206
-  "@angular/core": "2.3.1",
207
-  "@angular/forms": "2.3.1",
208
-  "@angular/http": "2.3.1",
209
-  "@angular/platform-browser": "2.3.1",
210
-  "@angular/platform-browser-dynamic": "2.3.1",
211
-  "@angular/platform-server": "2.3.1",
43
+  "@angular/common": "2.3.1"
212
 ```
44
 ```
213
 
45
 
214
 Run `yarn` to update to these versions.
46
 Run `yarn` to update to these versions.
257
 export class AppModule {}
89
 export class AppModule {}
258
 ```
90
 ```
259
 
91
 
260
-To render a login page before users can view the application, modify `src/app/app.component.ts` to use the `Stormpath` service and navigate to Stormpath's `LoginPage` if the user is not authenticated. 
261
-
262
-→ **io-app**
92
+To render a login page before users can view the application, modify `src/app/app.component.ts` to use the `Stormpath` service and navigate to Stormpath's `LoginPage` if the user is not authenticated. → **io-app**
263
 
93
 
264
 ```typescript
94
 ```typescript
265
-import { Component } from '@angular/core';
266
-import { Platform } from 'ionic-angular';
267
-import { StatusBar, Splashscreen } from 'ionic-native';
268
-import { TabsPage } from '../pages/tabs/tabs';
269
-import { Stormpath } from 'angular-stormpath';
270
-import { LoginPage } from 'angular-stormpath-ionic';
271
-
272
-@Component({
273
-  templateUrl: 'app.html'
274
-})
275
-export class MyApp {
276
-  rootPage;
277
-
278
-  constructor(platform: Platform, private stormpath: Stormpath) {
279
-    stormpath.user$.subscribe(user => {
280
-      if (!user) {
281
-        this.rootPage = LoginPage;
282
-      } else {
283
-        this.rootPage = TabsPage;
284
-      }
285
-    });
95
+stormpath.user$.subscribe(user => {
96
+  if (!user) {
97
+    this.rootPage = LoginPage;
98
+  } else {
99
+    this.rootPage = TabsPage;
286
   }
100
   }
287
-}
101
+});
288
 ```
102
 ```
289
 
103
 
290
-If you run `ionic serve`, you’ll likely see something similar to the following error in your browser’s console.
291
-
292
-```
293
-XMLHttpRequest cannot load http://localhost:8080/me. Response to preflight request
294
-doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on 
295
-the requested resource. Origin 'http://localhost:8100 is therefore not allowed access. 
296
-The response had HTTP status code 403.
297
-```
298
-
299
-To fix this, open your Spring Boot application's `src/main/resources/application.properties` and add the following line. This enables cross-origin resource sharing (CORS) from both the browser and the mobile client.
104
+If you run `ionic serve`, you’ll likely see a CORS error in your browser’s console. To fix this, open your Spring Boot application's `src/main/resources/application.properties` and add the following line.
300
 
105
 
301
 ```
106
 ```
302
 stormpath.web.cors.allowed.originUris = http://localhost:8100,file://
107
 stormpath.web.cors.allowed.originUris = http://localhost:8100,file://
304
 
109
 
305
 Restart Spring Boot and your Ionic app. 
110
 Restart Spring Boot and your Ionic app. 
306
 
111
 
307
-In `src/pages/home.html`, add a logout link to the header and a paragraph in the content section that shows the currently logged in user.
308
-
309
-→ **io-logout**
310
-
311
-```html
312
-<ion-header>
313
-  <ion-navbar>
314
-    <ion-title>Home</ion-title>
315
-    <ion-buttons end>
316
-      <button ion-button icon-only (click)="logout()">
317
-        Logout
318
-      </button>
319
-    </ion-buttons>
320
-  </ion-navbar>
321
-</ion-header>
322
-```
323
-
324
-→ **io-username**
325
-
326
-```html
327
-<ion-content padding>
328
-  ...
329
-  <p *ngIf="(user$ | async)">
330
-    You are logged in as: <b>{{ ( user$ | async ).fullName }}</b>
331
-  </p>
332
-</ion-content>
333
-```
112
+In `src/pages/home.html`, add a logout link to the header and a paragraph in the content section that shows the currently logged in user. → **io-logout** and **io-username**
334
 
113
 
335
-If you login, the “Logout” button will render, but won’t work because there’s no `logout()` method in `src/pages/home.ts`. Similarly, the “You are logged in” message won’t appear because there’s no `user$` variable defined. Change the body of `home.ts` to retrieve `user$` from the `Stormpath` service and define the `logout()` method.
336
-
337
-→ **io-home**
338
-
339
-```typescript
340
-import { Account, Stormpath } from 'angular-stormpath';
341
-import { Observable } from 'rxjs';
342
-...
343
-export class HomePage {
344
-  user$: Observable<Account | boolean>;
345
-
346
-  constructor(private stormpath: Stormpath) {
347
-    this.user$ = this.stormpath.user$;
348
-  }
349
-
350
-  logout(): void {
351
-    this.stormpath.logout();
352
-  }
353
-}
354
-```
114
+Change the body of `home.ts` to retrieve `user$` from the `Stormpath` service and define the `logout()` method. → **io-home**
355
 
115
 
356
 If you’re logged in, you should see a screen with a logout button and the name of the currently logged in user.
116
 If you’re logged in, you should see a screen with a logout button and the name of the currently logged in user.
357
 
117
 
358
-The `LoginPage` tries to auto-focus onto the `email` field when it loads. To auto-activate the keyboard you'll need to tell Cordova it’s OK to display the keyboard without user interaction. You can do this by adding the following to `config.xml` in the root directory.
359
-
360
-→ **io-keyboard**
118
+The `LoginPage` tries to auto-focus onto the `email` field when it loads. Tell Cordova it’s OK to display the keyboard without user interaction by adding the following to `config.xml` in the root directory. → **io-keyboard**
361
 
119
 
362
 ```xml
120
 ```xml
363
 <preference name="KeyboardDisplayRequiresUserAction" value="false"/>
121
 <preference name="KeyboardDisplayRequiresUserAction" value="false"/>
372
 
130
 
373
 ## Build a Good Beers UI
131
 ## Build a Good Beers UI
374
 
132
 
375
-Run `ionic generate page beer` to create a component and a template to display the list of good beers. 
376
-
377
-Add `BeerPage` to the `declarations` and `entryComponent` lists in `app.module.ts`.
378
-
379
-Run `ionic generate provider beer-service` to create a service to fetch the beer list from the Spring Boot API.
380
-
381
-Change `src/providers/beer-service.ts` to use have a `getGoodBeers()` method.
382
-
383
-→ **io-service**
384
-
385
-```typescript
386
-import { Injectable } from '@angular/core';
387
-import { Http, Response } from '@angular/http';
388
-import 'rxjs/add/operator/map';
389
-import { Observable } from 'rxjs';
390
-import { StormpathConfiguration } from 'angular-stormpath';
391
-
392
-@Injectable()
393
-export class BeerService {
394
-  public API;
395
-  public BEER_API;
396
-
397
-  constructor(public http: Http, public config: StormpathConfiguration) {
398
-    this.API = config.endpointPrefix;
399
-    this.BEER_API = this.API + '/beers';
400
-  }
401
-
402
-  getGoodBeers(): Observable<any> {
403
-    return this.http.get(this.API + '/good-beers')
404
-      .map((response: Response) => response.json());
405
-  }
406
-}
407
-```
408
-
409
-Modify `beer.html` to show the list of beers.
133
+1. Run `ionic generate page beer` to create a component and a template to display the list of good beers. 
134
+2. Add `BeerPage` to the `declarations` and `entryComponent` lists in `app.module.ts`.
135
+3. Run `ionic generate provider beer-service` to create a service to fetch the beer list from the Spring Boot API.
136
+4. Change `src/providers/beer-service.ts` to use have a `getGoodBeers()` method. → **io-beer-service**
137
+5. Modify `beer.html` to show the list of beers. → **io-beer-list**
138
+6. Update `beer.ts` to import `BeerService` and add as a provider. Call the `getGoodBeers()` method in the `ionViewDidLoad()` lifecycle method. → **io-get-good-beers**
139
+7. To expose this page on the tab bar, add it to `tabs.ts`. Update `tabs.html` too!
410
 
140
 
411
-→ **io-beer-list**
141
+Add some fun with Giphy! Run `ionic generate provider giphy-service`. → **ng-giphy-service**
412
 
142
 
413
-```html
414
-<ion-header>
415
-  <ion-navbar>
416
-    <ion-title>Good Beers</ion-title>
417
-  </ion-navbar>
418
-
419
-</ion-header>
420
-
421
-<ion-content padding>
422
-  <ion-list>
423
-    <ion-item *ngFor="let beer of beers" >
424
-      <ion-item>
425
-        <h2>{{beer.name}}</h2>
426
-      </ion-item>
427
-    </ion-item>
428
-  </ion-list>
429
-</ion-content>
430
-```
143
+Update `beer.ts` to take advantage of `GiphyService`. → **ng-giphy-foreach**
431
 
144
 
432
-Update `beer.ts` to import `BeerService` and add as a provider. Call the `getGoodBeers()` method in the `ionViewDidLoad()` lifecycle method.
433
-
434
-```typescript
435
-import { Component } from '@angular/core';
436
-import { BeerService } from '../../providers/beer-service';
437
-
438
-@Component({
439
-  selector: 'page-beer',
440
-  templateUrl: 'beer.html',
441
-  providers: [BeerService]
442
-})
443
-export class BeerPage {
444
-  beers: Array<any>;
445
-
446
-  constructor(public beerService: BeerService) {
447
-  }
448
-
449
-  ionViewDidLoad() {
450
-    this.beerService.getGoodBeers().subscribe(beers => {
451
-      this.beers = beers;
452
-    })
453
-  }
454
-}
455
-```
456
-
457
-To expose this page on the tab bar, add it to `tabs.ts`
458
-
459
-```typescript
460
-import { Component } from '@angular/core';
461
-
462
-import { HomePage } from '../home/home';
463
-import { AboutPage } from '../about/about';
464
-import { ContactPage } from '../contact/contact';
465
-import { BeerPage } from '../beer/beer';
466
-
467
-@Component({
468
-  templateUrl: 'tabs.html'
469
-})
470
-export class TabsPage {
471
-  // this tells the tabs component which Pages
472
-  // should be each tab's root Page
473
-  tab1Root: any = HomePage;
474
-  tab2Root: any = BeerPage;
475
-  tab3Root: any = ContactPage;
476
-  tab4Root: any = AboutPage;
477
-
478
-  constructor() {
479
-  }
480
-}
481
-```
482
-
483
-Update `tabs.html` too!
484
-
485
-```html
486
-<ion-tabs>
487
-  <ion-tab [root]="tab1Root" tabTitle="Home" tabIcon="home"></ion-tab>
488
-  <ion-tab [root]="tab2Root" tabTitle="Beer" tabIcon="beer"></ion-tab>
489
-  <ion-tab [root]="tab3Root" tabTitle="Contact" tabIcon="contacts"></ion-tab>
490
-  <ion-tab [root]="tab4Root" tabTitle="About" tabIcon="information-circle"></ion-tab>
491
-</ion-tabs>
492
-```
493
-
494
-Add some fun with Giphy! Run `ionic generate provider giphy-service`. Replace the code in `src/providers/giphy-service.ts` with the following TypeScript:
495
-
496
-→ **ng-giphy-service**
497
-
498
-```typescript
499
-import { Injectable } from '@angular/core';
500
-import { Http, Response } from '@angular/http';
501
-import { Observable } from 'rxjs';
502
-
503
-@Injectable()
504
-// http://tutorials.pluralsight.com/front-end-javascript/getting-started-with-angular-2-by-building-a-giphy-search-application
505
-export class GiphyService {
506
-
507
-  giphyApi = 'https://api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&q=';
508
-
509
-  constructor(public http: Http) {
510
-  }
511
-
512
-  get(searchTerm): Observable<any> {
513
-    let apiLink = this.giphyApi + searchTerm;
514
-    return this.http.request(apiLink).map((res: Response) => {
515
-      let results = res.json().data;
516
-      if (results.length > 0) {
517
-        return results[0].images.original.url;
518
-      } else {
519
-        return 'https://media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif'; // dancing cat for 404
520
-      }
521
-    });
522
-  }
523
-}
524
-```
525
-
526
-Update `beer.ts` to take advantage of `GiphyService`:
527
-
528
-→ **ng-giphy-foreach**
529
-
530
-```typescript
531
-import { Component } from '@angular/core';
532
-import { BeerService } from '../../providers/beer-service';
533
-import { GiphyService } from '../../providers/giphy-service';
534
-
535
-@Component({
536
-  selector: 'page-beer',
537
-  templateUrl: 'beer.html',
538
-  providers: [BeerService, GiphyService]
539
-})
540
-export class BeerPage {
541
-  private beers: Array<any>;
542
-
543
-  constructor(public beerService: BeerService, public giphyService: GiphyService) {
544
-  }
545
-
546
-  ionViewDidLoad() {
547
-    this.beerService.getGoodBeers().subscribe(beers => {
548
-      this.beers = beers;
549
-      for (let beer of this.beers) {
550
-        this.giphyService.get(beer.name).subscribe(url => {
551
-          beer.giphyUrl = url
552
-        });
553
-      }
554
-    })
555
-  }
556
-}
557
-```
558
-
559
-Update `beer.html` to display the image retrieved:
560
-
561
-→ **io-avatar**
562
-
563
-```html
564
-<ion-item>
565
-    <ion-avatar item-left>
566
-      <img src="{{beer.giphyUrl}}">
567
-    </ion-avatar>
568
-    <h2>{{beer.name}}</h2>
569
-</ion-item>
570
-```
145
+Update `beer.html` to display the image retrieved. → **io-avatar**
571
 
146
 
572
 If everything works as expected, you should see a page with a list of beers and images.
147
 If everything works as expected, you should see a page with a list of beers and images.
573
 
148
 
574
 ### Add a Modal for Editing
149
 ### Add a Modal for Editing
575
 
150
 
576
-Change the header in `beer.html` to have a button that opens a modal to add a new beer.
577
-
578
-→ **io-open-modal**
579
-
580
-```html
581
-<ion-header>
582
-  <ion-navbar>
583
-    <ion-title>Good Beers</ion-title>
584
-    <ion-buttons end>
585
-      <button ion-button icon-only (click)="openModal()" color="primary">
586
-        <ion-icon name="add-circle"></ion-icon>
587
-        <ion-icon name="beer"></ion-icon>
588
-      </button>
589
-    </ion-buttons>
590
-  </ion-navbar>
591
-```
151
+Change the header in `beer.html` to have a button that opens a modal to add a new beer. → **io-open-modal**
592
 
152
 
593
 In this same file, change `<ion-item>` to have a click handler for opening the modal for the current item.
153
 In this same file, change `<ion-item>` to have a click handler for opening the modal for the current item.
594
 
154
 
596
 <ion-item (click)="openModal({id: beer.id})">
156
 <ion-item (click)="openModal({id: beer.id})">
597
 ```
157
 ```
598
 
158
 
599
-Add `ModalController` as a dependency in `BeerPage` and add an `openModal()` method.
600
-
601
-→ **io-open-modal-ts**
602
-
603
-```typescript
604
-export class BeerPage {
605
-  private beers: Array<any>;
606
-
607
-  constructor(public beerService: BeerService, public giphyService: GiphyService,
608
-              public modalCtrl: ModalController) {
609
-  }
610
-
611
-  // ionViewDidLoad method
612
-
613
-  openModal(beerId) {
614
-    let modal = this.modalCtrl.create(BeerModalPage, beerId);
615
-    modal.present();
616
-    // refresh data after modal dismissed
617
-    modal.onDidDismiss(() => this.ionViewDidLoad())
618
-  }
619
-}
620
-```
621
-
622
-This won't compile because `BeerModalPage` doesn't exist. Create `beer-modal.ts` in the same directory. This page will retrieve the beer from the `beerId` that's passed in. It will render the name, allow it to be edited, and show the Giphy image found for the name.
623
-
624
-→ **io-beer-modal**
625
-
626
-```typescript
627
-import { BeerService } from '../../providers/beer-service';
628
-import { Component, ViewChild } from '@angular/core';
629
-import { GiphyService } from '../../providers/giphy-service';
630
-import { NavParams, ViewController, ToastController, NavController } from 'ionic-angular';
631
-import { NgForm } from '@angular/forms';
632
-
633
-@Component({
634
-  templateUrl: './beer-modal.html'
635
-})
636
-export class BeerModalPage {
637
-  @ViewChild('name') name;
638
-  beer: any = {};
639
-  error: any;
640
-
641
-  constructor(public beerService: BeerService,
642
-              public giphyService: GiphyService,
643
-              public params: NavParams,
644
-              public viewCtrl: ViewController,
645
-              public toastCtrl: ToastController,
646
-              public navCtrl: NavController) {
647
-    if (this.params.data.id) {
648
-      this.beerService.get(this.params.get('id')).subscribe(beer => {
649
-        this.beer = beer;
650
-        this.beer.href = beer._links.self.href;
651
-        this.giphyService.get(beer.name).subscribe(url => beer.giphyUrl = url);
652
-      });
653
-    }
654
-  }
655
-
656
-  dismiss() {
657
-    this.viewCtrl.dismiss();
658
-  }
659
-
660
-  save(form: NgForm) {
661
-    let update: boolean = form['href'];
662
-    this.beerService.save(form).subscribe(result => {
663
-      let toast = this.toastCtrl.create({
664
-        message: 'Beer "' + form.name + '" ' + ((update) ? 'updated' : 'added') + '.',
665
-        duration: 2000
666
-      });
667
-      toast.present();
668
-      this.dismiss();
669
-    }, error => this.error = error)
670
-  }
671
-
672
-  ionViewDidLoad() {
673
-    setTimeout(() => {
674
-      this.name.setFocus();
675
-    },150);
676
-  }
677
-}
678
-```
679
-
680
-Create `beer-modal.html` as a template for this page.
159
+Add `ModalController` as a dependency in `BeerPage` and add an `openModal()` method. → **io-open-modal-ts**
681
 
160
 
682
-→ **io-beer-modal-html**
161
+This won't compile because `BeerModalPage` doesn't exist. Create `beer-modal.ts` in the same directory. → **io-beer-modal**
683
 
162
 
684
-```html
685
-<ion-header>
686
-  <ion-toolbar>
687
-    <ion-title>
688
-      {{beer ? 'Beer Details' : 'Add Beer'}}
689
-    </ion-title>
690
-    <ion-buttons start>
691
-      <button ion-button (click)="dismiss()">
692
-        <span ion-text color="primary" showWhen="ios,core">Cancel</span>
693
-        <ion-icon name="md-close" showWhen="android,windows"></ion-icon>
694
-      </button>
695
-    </ion-buttons>
696
-  </ion-toolbar>
697
-</ion-header>
698
-<ion-content padding>
699
-  <form #beerForm="ngForm" (ngSubmit)="save(beerForm.value)">
700
-    <input type="hidden" name="href" [(ngModel)]="beer.href">
701
-    <ion-row>
702
-      <ion-col>
703
-        <ion-list inset>
704
-          <ion-item>
705
-            <ion-input placeholder="Beer Name" name="name" type="text"
706
-                       required [(ngModel)]="beer.name" #name></ion-input>
707
-          </ion-item>
708
-        </ion-list>
709
-      </ion-col>
710
-    </ion-row>
711
-    <ion-row>
712
-      <ion-col *ngIf="beer" text-center>
713
-        <img src="{{beer.giphyUrl}}">
714
-      </ion-col>
715
-    </ion-row>
716
-    <ion-row>
717
-      <ion-col>
718
-        <div *ngIf="error" class="alert alert-danger">{{error}}</div>
719
-        <button ion-button color="primary" full type="submit"
720
-                [disabled]="!beerForm.form.valid">Save</button>
721
-      </ion-col>
722
-    </ion-row>
723
-  </form>
724
-</ion-content>
725
-```
163
+Create `beer-modal.html` as a template for this page. → **io-beer-modal-html**
726
 
164
 
727
 Add `BeerModalPage` to the `declarations` and `entryComponent` lists in `app.module.ts`.
165
 Add `BeerModalPage` to the `declarations` and `entryComponent` lists in `app.module.ts`.
728
 
166
 
729
-You'll also need to modify `beer-service.ts` to have `get()` and `save()` methods.
730
-
731
-→ **io-get-save**
732
-
733
-```typescript
734
-get(id: string) {
735
-  return this.http.get(this.BEER_API + '/' + id)
736
-    .map((response: Response) => response.json());
737
-}
738
-
739
-save(beer: any): Observable<any> {
740
-  let result: Observable<Response>;
741
-  if (beer['href']) {
742
-    result = this.http.put(beer.href, beer);
743
-  } else {
744
-    result = this.http.post(this.BEER_API, beer)
745
-  }
746
-  return result.map((response: Response) => response.json())
747
-    .catch(error => Observable.throw(error));
748
-}
749
-
750
-remove(id: string) {
751
-  return this.http.delete(this.BEER_API + '/' + id)
752
-    .map((response: Response) => response.json());
753
-}
754
-```
167
+You'll also need to modify `beer-service.ts` to have `get()` and `save()` methods. → **io-get-save**
755
 
168
 
756
 ### Add Swipe to Delete
169
 ### Add Swipe to Delete
757
 
170
 
758
-To add swipe-to-delete functionality on the list of beers, open `beer.html` and make it so `<ion-item-sliding>` wraps `<ion-item>` and contains the `*ngFor`. Add a delete button using `<ion-item-options>`.
759
-
760
-→ **io-swipe**
761
-
762
-```html
763
-<ion-content padding>
764
-  <ion-list>
765
-    <ion-item-sliding *ngFor="let beer of beers">
766
-      <ion-item (click)="openModal({id: beer.id})">
767
-        <ion-avatar item-left>
768
-          <img src="{{beer.giphyUrl}}">
769
-        </ion-avatar>
770
-        <h2>{{beer.name}}</h2>
771
-      </ion-item>
772
-      <ion-item-options>
773
-        <button ion-button color="danger" (click)="remove(beer)"><ion-icon name="trash"></ion-icon> Delete</button>
774
-      </ion-item-options>
775
-    </ion-item-sliding>
776
-  </ion-list>
777
-</ion-content>
778
-```
779
-
780
-Add a `remove()` method to `beer.ts`. 
781
-
782
-→ **io-remove**
783
-
784
-```typescript
785
-remove(beer) {
786
-  this.beerService.remove(beer.id).subscribe(response => {
787
-    for (let i = 0; i < this.beers.length; i++) {
788
-      if (this.beers[i] === beer) {
789
-        this.beers.splice(i, 1);
790
-        let toast = this.toastCtrl.create({
791
-          message: 'Beer "' + beer.name + '" deleted.',
792
-          duration: 2000,
793
-          position: 'top'
794
-        });
795
-        toast.present();
796
-      }
797
-    }
798
-  });
799
-}
800
-```
171
+To add swipe-to-delete functionality on the list of beers, open `beer.html` and make it so `<ion-item-sliding>` wraps `<ion-item>` and contains the `*ngFor`. → **io-swipe**
801
 
172
 
802
-Add `toastCtrl` as a dependency in the constructor so everything compiles.
173
+Add a `remove()` method to `beer.ts`. → **io-remove**
803
 
174
 
804
-```typescript
805
-constructor(public beerService: BeerService, public giphyService: GiphyService,
806
-          public modalCtrl: ModalController, public toastCtrl: ToastController) {
807
-}
808
-```
175
+Add `toastCtrl: ToastController` as a dependency in the constructor so everything compiles.
809
 
176
 
810
 After making these additions, you should be able to add, edit and delete beers.
177
 After making these additions, you should be able to add, edit and delete beers.
811
 
178
 
812
 ## PWAs with Ionic
179
 ## PWAs with Ionic
813
 
180
 
814
-Ionic 2 ships with support for creating progressive web apps (PWAs). Run the [Lighthouse Chrome extension](https://developers.google.com/web/tools/lighthouse/) on this application.
815
-
816
-To register a service worker, and improve the app’s score, uncomment the following block in `index.html`.
817
-
818
-```html
819
-<!-- un-comment this code to enable service worker
820
-<script>
821
-  if ('serviceWorker' in navigator) {
822
-    navigator.serviceWorker.register('service-worker.js')
823
-      .then(() => console.log('service worker installed'))
824
-      .catch(err => console.log('Error', err));
825
-  }
826
-</script>-->
827
-```
181
+Run the [Lighthouse Chrome extension](https://developers.google.com/web/tools/lighthouse/) on this application. To register a service worker, and improve the app’s score, uncomment the `serviceWorker` block in `index.html`.
828
 
182
 
829
 After making this change, the score should improve. In my tests, it increased to 69/100.  
183
 After making this change, the score should improve. In my tests, it increased to 69/100.  
830
 
184
 
845
 
199
 
846
 To emulate or deploy to an iOS device, you’ll need a Mac and a fresh installation of [Xcode](https://developer.apple.com/xcode/). If you’d like to build iOS apps on Windows, Ionic offers an [Ionic Package](http://ionic.io/cloud#packaging) service.
200
 To emulate or deploy to an iOS device, you’ll need a Mac and a fresh installation of [Xcode](https://developer.apple.com/xcode/). If you’d like to build iOS apps on Windows, Ionic offers an [Ionic Package](http://ionic.io/cloud#packaging) service.
847
 
201
 
848
-Make sure to open Xcode to complete the installation.
849
-
850
 ```
202
 ```
851
 ionic platform add ios
203
 ionic platform add ios
852
 ```
204
 ```
866
 
218
 
867
 Select your phone as the target in Xcode and click the play button to run your app. The first time you do this, Xcode may spin for a while with a “Processing symbol files” message at the top.
219
 Select your phone as the target in Xcode and click the play button to run your app. The first time you do this, Xcode may spin for a while with a “Processing symbol files” message at the top.
868
 
220
 
869
-See Ionic’s [deploying documentation](https://ionicframework.com/docs/v2/setup/deploying/) for information on code signing and trusting the app’s certificate.
870
-
871
 Once you’re configured your phone, computer, and Apple ID to work, you should be able to open the app and see login, register, and forgot password screens.
221
 Once you’re configured your phone, computer, and Apple ID to work, you should be able to open the app and see login, register, and forgot password screens.
872
 
222
 
873
 ### Android
223
 ### Android