|
@@ -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
|