Browse Source

Remove unnecessary code listings

Matt Raible 7 years ago
parent
commit
60fe89870b
1 changed files with 38 additions and 688 deletions
  1. 38
    688
      DEMO.md

+ 38
- 688
DEMO.md View File

@@ -1,8 +1,6 @@
1 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 5
 ## Spring Boot API
8 6
 
@@ -13,166 +11,12 @@ http https://start.spring.io/starter.zip \/
13 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 21
 Access the API using `http localhost:8080/good-beers --auth <user>:<password>`.
178 22
 
@@ -188,10 +32,6 @@ From a terminal window, create a new application using the following command:
188 32
 
189 33
 ```
190 34
 ionic start ionic-beer --v2
191
-```
192
-
193
-This may take a minute or two to complete.
194
-```
195 35
 cd ionic-beer
196 36
 ionic serve
197 37
 ```
@@ -200,15 +40,7 @@ ionic serve
200 40
 
201 41
 ```json
202 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 46
 Run `yarn` to update to these versions.
@@ -257,46 +89,19 @@ export function stormpathConfig(): StormpathConfiguration {
257 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 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 107
 stormpath.web.cors.allowed.originUris = http://localhost:8100,file://
@@ -304,60 +109,13 @@ stormpath.web.cors.allowed.originUris = http://localhost:8100,file://
304 109
 
305 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 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 120
 ```xml
363 121
 <preference name="KeyboardDisplayRequiresUserAction" value="false"/>
@@ -372,223 +130,25 @@ git commit -m "Add Stormpath"
372 130
 
373 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 147
 If everything works as expected, you should see a page with a list of beers and images.
573 148
 
574 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 153
 In this same file, change `<ion-item>` to have a click handler for opening the modal for the current item.
594 154
 
@@ -596,235 +156,29 @@ In this same file, change `<ion-item>` to have a click handler for opening the m
596 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 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 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 177
 After making these additions, you should be able to add, edit and delete beers.
811 178
 
812 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 183
 After making this change, the score should improve. In my tests, it increased to 69/100.  
830 184
 
@@ -845,8 +199,6 @@ To see how your application will look on different devices you can run `ionic se
845 199
 
846 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 203
 ionic platform add ios
852 204
 ```
@@ -866,8 +218,6 @@ open ionic-auth.xcodeproj
866 218
 
867 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 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 223
 ### Android