Parcourir la source

Remove tutorial in favor of blog post

Matt Raible il y a 7 ans
Parent
révision
f1af8e5096
1 fichiers modifiés avec 0 ajouts et 849 suppressions
  1. 0
    849
      TUTORIAL.md

+ 0
- 849
TUTORIAL.md Voir le fichier

@@ -1,849 +0,0 @@
1
-# Tutorial: Develop a Mobile App with Ionic and Spring Boot
2
-
3
-Ionic 3.0 was [recently released](http://blog.ionic.io/ionic-3-0-has-arrived/), with support for 
4
-Angular 4, TypeScript 2.2, and lazy loading. Ionic, often called Ionic framework, is an open source
5
-(MIT-licensed) project that simplifies building native and progressive web apps. When developing an Ionic app, you'll use Angular and have access to native APIs via [Ionic Native](https://ionicframework.com/docs/native/) and [Apache Cordova](https://cordova.apache.org/). This means you can develop slick-looking UIs using the technologies you know and love: HTML, CSS, and JavaScript/TypeScript.
6
-
7
-This tutorial shows how to build a Spring Boot API, how to build an Ionic app, and how to deploy it to a mobile device.
8
-
9
-**Prerequisites**: [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) and [Node.js](https://nodejs.org) installed.
10
-
11
-## Create a Project
12
-
13
-To begin, create a directory on your hard drive called `spring-boot-ionic-example`. During this tutorial, you will create `server` and `ionic-beer` directories to hold the server and client applications, respectively.  
14
-
15
-## Spring Boot API
16
-
17
-I recently wrote about how to build a Spring Boot API in a [QuickStart Guide to Spring Boot with Angular]. Rather than covering that again, you can clone the existing project and copy the `server` directory into `spring-boot-ionic-example`.
18
-
19
-```bash
20
-git clone https://github.com/oktadeveloper/spring-boot-angular-example.git
21
-cp -r spring-boot-angular-example/server ~/spring-boot-ionic-example/.
22
-```
23
-
24
-This project contains a `beers` API that allows you to <abbr title="Create, Read, Update, and Delete">CRUD</abbr> a list of beer names. It also contains a `/good-beers` endpoint that filters out less-than-great beers.
25
-
26
-The default list of beers is created by a `BeerCommandLineRunner` class:
27
-
28
-```java
29
-@Component
30
-class BeerCommandLineRunner implements CommandLineRunner {
31
-    private final BeerRepository repository;
32
-
33
-    public BeerCommandLineRunner(BeerRepository repository) {
34
-        this.repository = repository;
35
-    }
36
-
37
-    @Override
38
-    public void run(String... strings) throws Exception {
39
-        // Top beers from https://www.beeradvocate.com/lists/top/
40
-        Stream.of("Kentucky Brunch Brand Stout", "Good Morning", "Very Hazy", "King Julius",
41
-                "Budweiser", "Coors Light", "PBR").forEach(name ->
42
-                repository.save(new Beer(name))
43
-        );
44
-        repository.findAll().forEach(System.out::println);
45
-    }
46
-}
47
-```
48
-
49
-The `BeerRepository` interface is decorated with `@RepositoryRestResource` to expose CRUD endpoints for the `Beer` entity.
50
-
51
-```java
52
-@RepositoryRestResource
53
-interface BeerRepository extends JpaRepository<Beer, Long> {}
54
-```
55
-
56
-The last piece of the API is the `BeerController` that exposes `/good-beers` and specifies cross-origin resource sharing (CORS) settings.
57
-
58
-```java
59
-@RestController
60
-public class BeerController {
61
-    private BeerRepository repository;
62
-
63
-    public BeerController(BeerRepository repository) {
64
-        this.repository = repository;
65
-    }
66
-
67
-    @GetMapping("/good-beers")
68
-    @CrossOrigin(origins = "http://localhost:4200")
69
-    public Collection<Map<String, String>> goodBeers() {
70
-
71
-        return repository.findAll().stream()
72
-                .filter(this::isGreat)
73
-                .map(b -> {
74
-                    Map<String, String> m = new HashMap<>();
75
-                    m.put("id", b.getId().toString());
76
-                    m.put("name", b.getName());
77
-                    return m;
78
-                }).collect(Collectors.toList());
79
-    }
80
-
81
-    private boolean isGreat(Beer beer) {
82
-        return !beer.getName().equals("Budweiser") &&
83
-                !beer.getName().equals("Coors Light") &&
84
-                !beer.getName().equals("PBR");
85
-    }
86
-}
87
-```
88
-
89
-You should be able to start the `server` application by running it in your favorite IDE or from the command line using `mvn spring-boot:run`. If you don't have Maven installed, you can use the Maven wrapper that's included in the project (`./mvnw spring-boot:run` on *nix, `\mvnw spring-boot:run` on Windows).
90
-
91
-After the app has started, navigate to <http://localhost:8080/good-beers>. You should see the list of good beers in your browser.
92
-
93
-![Good Beers JSON](static/good-beers-json.png) 
94
-
95
-## Create Ionic App
96
-
97
-To create an Ionic app to display data from your API, you'll first need to install Ionic CLI and Cordova: 
98
-
99
-```bash
100
-npm install -g ionic cordova
101
-```
102
-
103
-The [Ionic CLI](http://ionicframework.com/docs/cli/) is a command-line tool that greatly reduces the time it takes to develop an Ionic app. It's like a Swiss Army Knife: It brings together a bunch of miscellaneous tools under a single interface. The CLI contains a number of useful commands for Ionic development, such as `start`, `build`, `generate`, `serve`, and `run`.
104
-
105
-After installation completes, create a new application using the following command:
106
-
107
-```bash
108
-ionic start ionic-beer --v2
109
-```
110
-
111
-This may take a minute or two to complete, depending on your internet connection speed. In the same terminal window, change to be in your application's directory and run it.
112
-
113
-```bash
114
-cd ionic-beer
115
-ionic serve
116
-```
117
-
118
-This will open your default browser on [http://localhost:8100](http://localhost:8100). You can click through the tabbed interface to see the default structure of the app.
119
-
120
-![Ionic shell with tabs](static/ionic-tabs.png)
121
-
122
-## Create a Good Beers UI
123
-
124
-Run `ionic generate page beer` to create a component and a template to display the list of good beers. This creates a number of files in `src/app/pages/beer`:
125
-
126
-```
127
-beer.html
128
-beer.module.ts
129
-beer.scss
130
-beer.ts
131
-```
132
-
133
-Open `beer.ts` and change the name of the class to be `BeerPage`.
134
-
135
-```typescript
136
-export class BeerPage {
137
-
138
-  constructor(public navCtrl: NavController, public navParams: NavParams) {
139
-  }
140
-
141
-  ionViewDidLoad() {
142
-    console.log('ionViewDidLoad BeerPage');
143
-  }
144
-
145
-}
146
-```
147
-
148
-Modify `beer.module.ts` to change the class name too.
149
-
150
-```typescript
151
-import { NgModule } from '@angular/core';
152
-import { IonicModule } from 'ionic-angular';
153
-import { BeerPage } from './beer';
154
-
155
-@NgModule({
156
-  declarations: [
157
-    BeerPage
158
-  ],
159
-  imports: [
160
-    IonicModule.forRoot(BeerPage),
161
-  ],
162
-  exports: [
163
-    BeerPage
164
-  ]
165
-})
166
-export class BeerModule {}
167
-```
168
-
169
-Add `BeerModule` to the `imports` list in `app.module.ts`.
170
-
171
-```typescript
172
-import { BeerModule } from '../pages/beer/beer.module';
173
-
174
-@NgModule({
175
-  ...
176
-  imports: [
177
-    BrowserModule,
178
-    IonicModule.forRoot(MyApp),
179
-    BeerModule
180
-  ],
181
-  ...
182
-})
183
-```
184
-
185
-Run `ionic g provider beer-service` to create a service to fetch the beer list from the Spring Boot API.
186
-
187
-Change `src/providers/beer-service.ts` to have constants for the API path and add a `getGoodBeers()` method.
188
-
189
-```typescript
190
-import { Injectable } from '@angular/core';
191
-import { Http, Response } from '@angular/http';
192
-import 'rxjs/add/operator/map';
193
-import { Observable } from 'rxjs';
194
-
195
-@Injectable()
196
-export class BeerService {
197
-  public API = 'http://localhost:8080';
198
-  public BEER_API = this.API + '/beers';
199
-
200
-  constructor(private http: Http) {}
201
-
202
-  getGoodBeers(): Observable<any> {
203
-    return this.http.get(this.API + '/good-beers')
204
-      .map((response: Response) => response.json());
205
-  }
206
-}
207
-```
208
-
209
-Modify `beer.html` to show the list of beers.
210
-
211
-```html
212
-<ion-header>
213
-  <ion-navbar>
214
-    <ion-title>Good Beers</ion-title>
215
-  </ion-navbar>
216
-
217
-</ion-header>
218
-
219
-<ion-content padding>
220
-  <ion-list>
221
-    <ion-item *ngFor="let beer of beers">
222
-      <h2>{{beer.name}}</h2>
223
-    </ion-item>
224
-  </ion-list>
225
-</ion-content>
226
-```
227
-
228
-Modify `beer.module.ts` to import `BeerService` and add it as a provider. You could add it as a provider in each component, but adding it in the module allows all components to use it. 
229
-
230
-```typescript
231
-import { BeerService } from '../../providers/beer-service';
232
-
233
-@NgModule({
234
-  ...
235
-  providers: [
236
-    BeerService
237
-  ]
238
-})
239
-```
240
-
241
-Update `beer.ts` to import `BeerService` and add it as a dependency in the constructor. Call the `getGoodBeers()` method in the `ionViewDidLoad()` lifecycle method.
242
-
243
-```typescript
244
-import { Component } from '@angular/core';
245
-import { IonicPage, NavController, NavParams } from 'ionic-angular';
246
-import { BeerService } from '../../providers/beer-service';
247
-
248
-@IonicPage()
249
-@Component({
250
-  selector: 'page-beer',
251
-  templateUrl: 'beer.html'
252
-})
253
-export class BeerPage {
254
-  private beers: Array<any>;
255
-
256
-  constructor(public navCtrl: NavController, public navParams: NavParams,
257
-              public beerService: BeerService) {
258
-  }
259
-
260
-  ionViewDidLoad() {
261
-    this.beerService.getGoodBeers().subscribe(beers => {
262
-      this.beers = beers;
263
-    })
264
-  }
265
-}
266
-```
267
-
268
-To expose this page on the tab bar, add it to `tabs.ts`
269
-
270
-```typescript
271
-import { Component } from '@angular/core';
272
-
273
-import { HomePage } from '../home/home';
274
-import { AboutPage } from '../about/about';
275
-import { ContactPage } from '../contact/contact';
276
-import { BeerPage } from '../beer/beer';
277
-
278
-@Component({
279
-  templateUrl: 'tabs.html'
280
-})
281
-export class TabsPage {
282
-  tab1Root: any = HomePage;
283
-  tab2Root: any = BeerPage;
284
-  tab3Root: any = ContactPage;
285
-  tab4Root: any = AboutPage;
286
-
287
-  constructor() {}
288
-}
289
-```
290
-
291
-You'll also need to update `tabs.html` to have the new tab order.
292
-
293
-```html
294
-<ion-tabs>
295
-  <ion-tab [root]="tab1Root" tabTitle="Home" tabIcon="home"></ion-tab>
296
-  <ion-tab [root]="tab2Root" tabTitle="Beer" tabIcon="beer"></ion-tab>
297
-  <ion-tab [root]="tab3Root" tabTitle="Contact" tabIcon="contacts"></ion-tab>
298
-  <ion-tab [root]="tab4Root" tabTitle="About" tabIcon="information-circle"></ion-tab>
299
-</ion-tabs>
300
-```
301
-
302
-### Add Some Fun with Animated GIFs
303
-
304
-Run `ionic g provider giphy-service` to generate a `GiphyService` class. Replace the code in `src/providers/giphy-service.ts` with code that searches Giphy's API:
305
-
306
-```typescript
307
-import { Injectable } from '@angular/core';
308
-import { Http, Response } from '@angular/http';
309
-import { Observable } from 'rxjs';
310
-
311
-@Injectable()
312
-// http://tutorials.pluralsight.com/front-end-javascript/getting-started-with-angular-2-by-building-a-giphy-search-application
313
-export class GiphyService {
314
-
315
-  // Public beta key: https://github.com/Giphy/GiphyAPI#public-beta-key
316
-  giphyApi = 'https://api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&q=';
317
-
318
-  constructor(public http: Http) {
319
-  }
320
-
321
-  get(searchTerm): Observable<any> {
322
-    let apiLink = this.giphyApi + searchTerm;
323
-    return this.http.request(apiLink).map((res: Response) => {
324
-      let results = res.json().data;
325
-      if (results.length > 0) {
326
-        return results[0].images.original.url;
327
-      } else {
328
-        return 'https://media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif'; // dancing cat for 404
329
-      }
330
-    });
331
-  }
332
-}
333
-```
334
-
335
-Update `beer.module.ts` to import `GiphyService` and include it is as a provider.
336
-
337
-```typescript
338
-import { GiphyService } from '../../providers/giphy-service';
339
-
340
-@NgModule({
341
-  ...
342
-  providers: [
343
-    BeerService,
344
-    GiphyService
345
-  ]
346
-})
347
-```
348
-
349
-Modify `beer.ts` to import `GiphyService` and set a `giphyUrl` on each beer.
350
-
351
-```typescript
352
-import { Component } from '@angular/core';
353
-import { IonicPage, NavController, NavParams } from 'ionic-angular';
354
-import { BeerService } from '../../providers/beer-service';
355
-import { GiphyService } from '../../providers/giphy-service';
356
-
357
-@IonicPage()
358
-@Component({
359
-  selector: 'page-beer',
360
-  templateUrl: 'beer.html'
361
-})
362
-export class BeerPage {
363
-  private beers: Array<any>;
364
-
365
-  constructor(public navCtrl: NavController, public navParams: NavParams,
366
-              public beerService: BeerService, public giphyService: GiphyService) {
367
-  }
368
-
369
-  ionViewDidLoad() {
370
-    this.beerService.getGoodBeers().subscribe(beers => {
371
-      this.beers = beers;
372
-      for (let beer of this.beers) {
373
-        this.giphyService.get(beer.name).subscribe(url => {
374
-          beer.giphyUrl = url
375
-        });
376
-      }
377
-    })
378
-  }
379
-}
380
-```
381
-
382
-Update `beer.html` to display the image retrieved:
383
-
384
-```html
385
-<ion-item *ngFor="let beer of beers">
386
-  <ion-avatar item-left>
387
-    <img src="{{beer.giphyUrl}}">
388
-  </ion-avatar>
389
-  <h2>{{beer.name}}</h2>
390
-</ion-item>
391
-```
392
-
393
-Start the Spring Boot app in one terminal and run `ionic serve` in another. Open <http://localhost:8100> in your browser. Click on the Beer icon and you'll likely see an error in your browser.
394
-
395
-```
396
-Uncaught (in promise): Error: No provider for Http! 
397
-```
398
-
399
-![No provider for Http!](static/no-http-provider.png)
400
-
401
-This highlights one of the slick features of Ionic: errors are displayed in your browser, not just the browser's console. Add `HttpModule` to the list of imports in `src/app/app.module.ts` to solve this issue.
402
-
403
-```typescript
404
-import { HttpModule } from '@angular/http';
405
-
406
-@NgModule({
407
-  ...
408
-  imports: [
409
-    BrowserModule,
410
-    HttpModule,
411
-    IonicModule.forRoot(MyApp),
412
-    BeerModule
413
-  ],
414
-```
415
-
416
-After making this change, you'll likely see the following error in your browser's console.
417
-
418
-```
419
-XMLHttpRequest cannot load http://localhost:8080/good-beers. No 'Access-Control-Allow-Origin' 
420
-header is present on the requested resource. Origin 'http://localhost:8100' is therefore 
421
-not allowed access. The response had HTTP status code 401.
422
-```
423
-
424
-To fix this, open your Spring Boot application's `BeerController.java` class and change its `@CrossOrigin` annotation to allow `http://localhost:8100` and `file://`. This enables cross-origin resource sharing (CORS) from both the browser and the mobile client.
425
-
426
-```java
427
-@CrossOrigin(origins = {"http://localhost:8100","file://"})
428
-public Collection<Map<String, String>> goodBeers() {
429
-```
430
-
431
-Recompile this class and DevTools should restart the application.
432
-
433
-If everything works as expected, you should see a page similar to the one below in your browser.
434
-
435
-![Good Beers UI](static/good-beers-ui.png)
436
-
437
-### Add a Modal for Editing
438
-
439
-Change the header in `beer.html` to have a button that opens a modal to add a new beer.
440
-
441
-```html
442
-<ion-header>
443
-  <ion-navbar>
444
-    <ion-title>Good Beers</ion-title>
445
-    <ion-buttons end>
446
-      <button ion-button icon-only (click)="openModal()" color="primary">
447
-        <ion-icon name="add-circle"></ion-icon>
448
-        <ion-icon name="beer"></ion-icon>
449
-      </button>
450
-    </ion-buttons>
451
-  </ion-navbar>
452
-```
453
-
454
-In this same file, change `<ion-item>` to have a click handler for opening the modal for the current item.
455
-
456
-```html
457
-<ion-item *ngFor="let beer of beers" (click)="openModal({id: beer.id})">
458
-```
459
-
460
-Add `ModalController` as a dependency in `BeerPage` and add an `openModal()` method.
461
-
462
-```typescript
463
-import { IonicPage, ModalController, NavController, NavParams } from 'ionic-angular';
464
-
465
-export class BeerPage {
466
-  private beers: Array<any>;
467
-
468
-  constructor(public navCtrl: NavController, public navParams: NavParams,
469
-              public beerService: BeerService, public giphyService: GiphyService,
470
-              public modalCtrl: ModalController) {
471
-  }
472
-
473
-  // ionViewDidLoad()
474
-
475
-  openModal(beerId) {
476
-    let modal = this.modalCtrl.create(BeerModalPage, beerId);
477
-    modal.present();
478
-    // refresh data after modal dismissed
479
-    modal.onDidDismiss(() => this.ionViewDidLoad())
480
-  }
481
-}
482
-```
483
-
484
-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.
485
-
486
-```typescript
487
-import { BeerService } from '../../providers/beer-service';
488
-import { Component, ViewChild } from '@angular/core';
489
-import { GiphyService } from '../../providers/giphy-service';
490
-import { NavParams, ViewController, ToastController, NavController } from 'ionic-angular';
491
-import { NgForm } from '@angular/forms';
492
-
493
-@Component({
494
-  templateUrl: './beer-modal.html'
495
-})
496
-export class BeerModalPage {
497
-  @ViewChild('name') name;
498
-  beer: any = {};
499
-  error: any;
500
-
501
-  constructor(public beerService: BeerService,
502
-              public giphyService: GiphyService,
503
-              public params: NavParams,
504
-              public viewCtrl: ViewController,
505
-              public toastCtrl: ToastController,
506
-              public navCtrl: NavController) {
507
-    if (this.params.data.id) {
508
-      this.beerService.get(this.params.get('id')).subscribe(beer => {
509
-        this.beer = beer;
510
-        this.beer.href = beer._links.self.href;
511
-        this.giphyService.get(beer.name).subscribe(url => beer.giphyUrl = url);
512
-      });
513
-    }
514
-  }
515
-
516
-  dismiss() {
517
-    this.viewCtrl.dismiss();
518
-  }
519
-
520
-  save(form: NgForm) {
521
-    let update: boolean = form['href'];
522
-    this.beerService.save(form).subscribe(result => {
523
-      let toast = this.toastCtrl.create({
524
-        message: 'Beer "' + form.name + '" ' + ((update) ? 'updated' : 'added') + '.',
525
-        duration: 2000
526
-      });
527
-      toast.present();
528
-      this.dismiss();
529
-    }, error => this.error = error)
530
-  }
531
-
532
-  ionViewDidLoad() {
533
-    setTimeout(() => {
534
-      this.name.setFocus();
535
-    },150);
536
-  }
537
-}
538
-```
539
-
540
-Add the import for `BeerModalPage` to `beer.ts`, then create `beer-modal.html` as a template for this page.
541
-
542
-```html
543
-<ion-header>
544
-  <ion-toolbar>
545
-    <ion-title>
546
-      {{beer ? 'Beer Details' : 'Add Beer'}}
547
-    </ion-title>
548
-    <ion-buttons start>
549
-      <button ion-button (click)="dismiss()">
550
-        <span ion-text color="primary" showWhen="ios,core">Cancel</span>
551
-        <ion-icon name="md-close" showWhen="android,windows"></ion-icon>
552
-      </button>
553
-    </ion-buttons>
554
-  </ion-toolbar>
555
-</ion-header>
556
-<ion-content padding>
557
-  <form #beerForm="ngForm" (ngSubmit)="save(beerForm.value)">
558
-    <input type="hidden" name="href" [(ngModel)]="beer.href">
559
-    <ion-row>
560
-      <ion-col>
561
-        <ion-list inset>
562
-          <ion-item>
563
-            <ion-input placeholder="Beer Name" name="name" type="text"
564
-                       required [(ngModel)]="beer.name" #name></ion-input>
565
-          </ion-item>
566
-        </ion-list>
567
-      </ion-col>
568
-    </ion-row>
569
-    <ion-row>
570
-      <ion-col *ngIf="beer" text-center>
571
-        <img src="{{beer.giphyUrl}}">
572
-      </ion-col>
573
-    </ion-row>
574
-    <ion-row>
575
-      <ion-col>
576
-        <div *ngIf="error" class="alert alert-danger">{{error}}</div>
577
-        <button ion-button color="primary" full type="submit"
578
-                [disabled]="!beerForm.form.valid">Save</button>
579
-      </ion-col>
580
-    </ion-row>
581
-  </form>
582
-</ion-content>
583
-```
584
-
585
-Add `BeerModalPage` to the `declarations` and `entryComponent` lists in `beer.module.ts`.
586
-
587
-You'll also need to modify `beer-service.ts` to have `get()` and `save()` methods.
588
-
589
-```typescript
590
-get(id: string) {
591
-  return this.http.get(this.BEER_API + '/' + id)
592
-    .map((response: Response) => response.json());
593
-}
594
-
595
-save(beer: any): Observable<any> {
596
-  let result: Observable<Response>;
597
-  if (beer['href']) {
598
-    result = this.http.put(beer.href, beer);
599
-  } else {
600
-    result = this.http.post(this.BEER_API, beer)
601
-  }
602
-  return result.map((response: Response) => response.json())
603
-    .catch(error => Observable.throw(error));
604
-}
605
-```
606
-
607
-At this point, if you try to add or edit a beer name, you'll likely see an error in your browser's console.
608
-
609
-```
610
-ModalCmp ionViewPreLoad error: No component factory found for BeerModalPage. 
611
-Did you add it to @NgModule.entryComponents?
612
-```
613
-
614
-Add `BeerModalPage` to the list of `entryComponents` in `app.module.ts`. While you're in there, add `BeerService` to the list of global providers so you don't have to add it to each component that uses it.
615
-
616
-```typescript
617
-@NgModule({
618
-  ...
619
-  entryComponents: [
620
-    ...
621
-    BeerModalPage
622
-  ],
623
-  providers: [
624
-    BeerService,
625
-    ...
626
-  ]
627
-})
628
-```
629
-
630
-Now if you try to edit a beer's name, you'll see another CORS in your browser's console. Add a `@CrossOrigin` annotation to `BeerRepository.java` (in your Spring Boot project) that matches the one in `BeerController`.
631
- 
632
-```java
633
-@RepositoryRestResource
634
-@CrossOrigin(origins = {"http://localhost:8100","file://"})
635
-```
636
-
637
-Re-compile and now everything should work as expected. For example, below is a screenshot that shows I added a new beer and what it looks like when editing it.
638
- 
639
-![Mmmmm, Guinness](static/beer-modal.png)
640
-
641
-### Add Swipe to Delete
642
-
643
-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>`.
644
-
645
-```html
646
-<ion-content padding>
647
-  <ion-list>
648
-    <ion-item-sliding *ngFor="let beer of beers">
649
-      <ion-item (click)="openModal({id: beer.id})">
650
-        <ion-avatar item-left>
651
-          <img src="{{beer.giphyUrl}}">
652
-        </ion-avatar>
653
-        <h2>{{beer.name}}</h2>
654
-      </ion-item>
655
-      <ion-item-options>
656
-        <button ion-button color="danger" (click)="remove(beer)"><ion-icon name="trash"></ion-icon> Delete</button>
657
-      </ion-item-options>
658
-    </ion-item-sliding>
659
-  </ion-list>
660
-</ion-content>
661
-```
662
-
663
-Add a `remove()` method to `beer.ts`.
664
-
665
-```typescript
666
-remove(beer) {
667
-  this.beerService.remove(beer.id).subscribe(response => {
668
-    for (let i = 0; i < this.beers.length; i++) {
669
-      if (this.beers[i] === beer) {
670
-        this.beers.splice(i, 1);
671
-        let toast = this.toastCtrl.create({
672
-          message: 'Beer "' + beer.name + '" deleted.',
673
-          duration: 2000,
674
-          position: 'top'
675
-        });
676
-        toast.present();
677
-      }
678
-    }
679
-  });
680
-}
681
-```
682
-
683
-Add `toastCtrl` as a dependency in the constructor so everything compiles.
684
-
685
-```typescript
686
-constructor(public navCtrl: NavController, public navParams: NavParams,
687
-          public beerService: BeerService, public giphyService: GiphyService,
688
-          public modalCtrl: ModalController, public toastCtrl: ToastController) {
689
-}
690
-```
691
-
692
-You'll also need to modify `beer-service.ts` to have a `remove()` method.
693
-
694
-```typescript
695
-remove(id: string) {
696
-  return this.http.delete(this.BEER_API + '/' + id)
697
-    .map((response: Response) => response.json());
698
-}
699
-```
700
-
701
-After making these additions, you should be able to delete beer names. To emulate a left swipe in your browser, click on the item and drag it to the left.
702
-
703
-![Left swipe](static/beer-delete.png)
704
-
705
-## PWAs with Ionic
706
-
707
-Ionic ships with support for creating progressive web apps (PWAs). If you'd like to learn more about what PWAs are, see [Navigating the World of Progressive Web Apps with Ionic 2](http://blog.ionic.io/navigating-the-world-of-progressive-web-apps-with-ionic-2/). This blog post is still relevant for Ionic 3.
708
-
709
-If you run the [Lighthouse Chrome extension](https://developers.google.com/web/tools/lighthouse/) on this application, you'll get a mediocre score (51/100).
710
-
711
-![Lighthouse: 51](static/lighthouse-51.png)
712
-
713
-To register a service worker, and improve the app's score, uncomment the following block in `src/index.html`.
714
-
715
-```html
716
-<!-- un-comment this code to enable service worker
717
-<script>
718
-if ('serviceWorker' in navigator) {
719
-  navigator.serviceWorker.register('service-worker.js')
720
-    .then(() => console.log('service worker installed'))
721
-    .catch(err => console.log('Error', err));
722
-}
723
-</script>-->
724
-```
725
-
726
-After making this change, the score should improve. In my tests, it increased to 66/100. The remaining issues were:
727
-
728
-* A couple -1's in performance for "Cannot read property 'ts' of undefined".
729
-* Site is not progressively enhanced (page should contain some content when JavaScript is not available). This could likely be solved with [Angular's app-shell directives](https://www.npmjs.com/package/@angular/app-shell).
730
-* Site is not on HTTPS and does not redirect HTTP to HTTPS.
731
-
732
-If you refresh the app and Chrome doesn't prompt you to install the app (a PWA feature), you probably need to turn on a couple of features. Copy and paste the following URLs into Chrome and enable each feature.
733
-
734
-```
735
-chrome://flags/#bypass-app-banner-engagement-checks
736
-chrome://flags/#enable-add-to-shelf
737
-```
738
-
739
-After enabling these flags, you'll see an error in your browser's console about `assets/imgs/logo.png` not being found. This file is referenced in `src/manifest.json`. You can fix this by copying a 512x512 PNG ([like this one](http://www.iconsdb.com/orange-icons/beer-icon.html)) into this location or by modifying `manifest.json` accordingly. At the very least, you should modify `manifest.json` to have your app's name.
740
-
741
-## Deploy to a Mobile Device
742
-
743
-It's pretty cool that you're able to develop mobile apps with Ionic in your browser. However, it's nice to see the fruits of your labor and see how awesome your app looks on a phone. It really does look and behave like a native app!
744
-
745
-To see how your application will look on different devices you can run `ionic serve --lab`. The `--lab` flag opens opens a page in your browser that lets you see how your app looks on different devices.
746
-
747
-![Ionic Labs](static/ionic-labs.png)
748
-
749
-### iOS
750
-
751
-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.
752
-
753
-Make sure to open Xcode to complete the installation.
754
-
755
-Ionic 3.0 generates iOS-enabled applications by default. If you're using a version that didn't generate a `platforms/ios` directory, add support for it using the following command.
756
-
757
-```
758
-ionic platform add ios
759
-```
760
-
761
-Then run `ionic emulate ios` to open your app in Simulator. 
762
-
763
-**TIP:** The biggest problem I found when running the app in Simulator was that it was difficult to get the keyboard to popup. To workaround this, I used **Hardware** > **Keyboard** > **Toggle Software Keyboard** when I needed to type text in a field.
764
-
765
-Deploying to your phone will likely fail because it won't be able to connect to `http://localhost:8080`. To fix this, you can deploy your Spring Boot app to a public server, or use your computer's IP address in `beer.service.ts` (if you're on the same network). 
766
-
767
-<div style="padding: 5px; background: #eee; border: 1px solid silver; margin-bottom: 10px">
768
-<p>To deploy to Cloud Foundry, copy <a href="https://github.com/oktadeveloper/spring-boot-ionic-example/blob/master/deploy.sh">this deploy script</a> to your hard drive. It expects to be in a directory above your apps (e.g. <code>spring-boot-ionic-example</code>). It also expects your apps to be named <code>ionic-beer</code> and <code>server</code>.
769
-</p>
770
-<p>
771
-If you don't have a Cloud Foundry account, you'll need to <a href="https://account.run.pivotal.io/z/uaa/sign-up">create one</a>. Then install its command line tools for this script to work.
772
-</p>
773
-<pre style="margin-bottom: 0">
774
-brew tap cloudfoundry/tap && brew install cf-cli
775
-</pre>
776
-</div>
777
-
778
-To deploy the app to an iPhone, start by plugging it into your computer. Then run the following commands to install ios-deploy/ios-sim, build the app, and run it on your device. 
779
-
780
-```
781
-npm install -g ios-deploy ios-sim
782
-ionic build ios --prod
783
-cd platforms/ios/
784
-open ionic-beer.xcodeproj
785
-```
786
-
787
-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.
788
-
789
-**NOTE:** If you run into code signing issues, see [Ionic's deployment documentation](http://ionicframework.com/docs/intro/deploying/#ios-devices) to see how to solve.
790
-
791
-Once you've configured your phone, computer, and Apple ID to work, you should be able to open the app and see the beer list you created. Below is how it looks on my iPhone 6s Plus.
792
-
793
-<img src="static/iphone-beer-list.jpg" style="width: 621px" alt="iPhone Beer List">
794
-
795
-### Android
796
-
797
-To emulate or deploy to an Android device, you'll first need to install [Android Studio](https://developer.android.com/studio/index.html). As part of the install, it will show you where it installed the Android SDK. Set this path as an ANDROID_HOME environment variable. On a Mac, it should be `~/Library/Android/sdk/`.
798
-
799
-If you've just installed Android Studio, make sure to open it to complete the installation.
800
-
801
-To deploy to the Android emulator, add support for it to the ionic-beer project using the `ionic` command.
802
-
803
-```
804
-ionic platform add android
805
-```
806
-
807
-If you run `ionic emulate android` you'll get instructions from about how to create an emulator image.
808
-
809
-```
810
-Error: No emulator images (avds) found.
811
-1. Download desired System Image by running: /Users/mraible/Library/Android/sdk/tools/android sdk
812
-2. Create an AVD by running: /Users/mraible/Library/Android/sdk/tools/android avd
813
-HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver
814
-```
815
-
816
-Run the first suggestion and download your desired system image. Then  run the second command and created an AVD (Android Virtual Device) with the following settings:
817
-
818
-```
819
-AVD Name: TestPhone
820
-Device: Nexus 5
821
-Target: Android 7.1.1
822
-CPU/ABI: Google APIs Intel Axom (x86_64)
823
-Skin: Skin with dynamic hardware controls
824
-```
825
-
826
-After performing these steps, you should be able to run `ionic emulate android` and see your app running in the AVD.
827
-
828
-<img src="static/android-beer-list.png" style="width: 540px" alt="Android Beer List">
829
-
830
-**NOTE**: If you get an application error that says "The connection to the server was unsuccessful. (file:///android/www/index.html)", add the following line to `config.xml`. This sets the default timeout to 60 seconds (default is 20). Thanks to [Stack Overflow](http://stackoverflow.com/a/31377846) for the answer.
831
-
832
-```xml
833
-<preference name="loadUrlTimeoutValue" value="60000"/>
834
-```
835
-
836
-## Learn More
837
-
838
-I hope you've enjoyed this tour of Ionic and Angular. I like how Ionic takes your web development skills up a notch and allows you to create mobile applications that look and behave natively.
839
-
840
-You can find a completed version of the application created in this blog post [on GitHub](https://github.com/oktadeveloper/spring-boot-ionic-example).
841
-
842
-If you encountered issues, please [create an issue in GitHub](https://github.com/oktadeveloper/spring-boot-ionic-example/issues/new) or hit me up on Twitter [@mraible](https://twitter.com/mraible).
843
-
844
-To learn more about Ionic or Angular, please see the following resources:
845
-
846
-* [Get started with Ionic Framework](http://ionicframework.com/getting-started/)
847
-* [Angular + Spring Boot](developer blog URL tbd)
848
-* [Angular Authentication with OpenID Connect and Okta in 20 Minutes](http://developer.okta.com/blog/2017/04/17/angular-authentication-with-oidc)
849
-* [Getting Started with Angular](https://www.youtube.com/watch?v=Jq3szz2KOOs) A YouTube webinar by yours truly. ;)