|
@@ -1,93 +1,31 @@
|
1
|
|
-# Spring Boot, Ionic, and Stormpath
|
|
1
|
+# Tutorial: Develop a Mobile App with Ionic and Spring Boot
|
2
|
2
|
|
3
|
|
-This tutorial shows how to build a secure Spring Boot API with Stormpath. It also shows how to build an Ionic app that securely connects to this API and can be deployed to a mobile device.
|
|
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.
|
4
|
6
|
|
5
|
|
-**Prerequisites**: Java 8, Node.js, Maven, a [Stormpath Account](https://api.stormpath.com/register), and an `apiKey.properties` file in `~/stormpath/`.
|
|
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.
|
6
|
8
|
|
7
|
|
-## Spring Boot API
|
8
|
|
-
|
9
|
|
-Create your Spring Boot API project using [start.spring.io](https://start.spring.io).
|
10
|
|
-
|
11
|
|
-```
|
12
|
|
-http https://start.spring.io/starter.zip \
|
13
|
|
-dependencies==data-jpa,data-rest,h2,web,devtools,security,stormpath -d
|
14
|
|
-```
|
15
|
|
-
|
16
|
|
-Run the application with `./mvnw spring-boot:run`.
|
17
|
|
-
|
18
|
|
-Create a `Beer` entity class in `src/main/java/com/example/beer`.
|
|
9
|
+**Prerequisites**: [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) and [Node.js](https://nodejs.org) installed.
|
19
|
10
|
|
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;
|
|
11
|
+## Create a Project
|
34
|
12
|
|
35
|
|
- public Beer() {
|
36
|
|
- }
|
37
|
|
-
|
38
|
|
- public Beer(String name) {
|
39
|
|
- this.name = name;
|
40
|
|
- }
|
|
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.
|
41
|
14
|
|
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
|
|
- }
|
|
15
|
+## Spring Boot API
|
53
|
16
|
|
54
|
|
- public void setName(String name) {
|
55
|
|
- this.name = name;
|
56
|
|
- }
|
|
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`.
|
57
|
18
|
|
58
|
|
- @Override
|
59
|
|
- public String toString() {
|
60
|
|
- return "Beer{" +
|
61
|
|
- "id=" + id +
|
62
|
|
- ", name='" + name + '\'' +
|
63
|
|
- '}';
|
64
|
|
- }
|
65
|
|
-}
|
|
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/.
|
66
|
22
|
```
|
67
|
23
|
|
68
|
|
-Create a JPA Repository to manage the `Beer` entity.
|
69
|
|
-
|
70
|
|
-```java
|
71
|
|
-package com.example.beer;
|
72
|
|
-
|
73
|
|
-import org.springframework.data.jpa.repository.JpaRepository;
|
74
|
|
-import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
|
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.
|
75
|
25
|
|
76
|
|
-@RepositoryRestResource
|
77
|
|
-interface BeerRepository extends JpaRepository<Beer, Long> {
|
78
|
|
-}
|
79
|
|
-```
|
80
|
|
-
|
81
|
|
-Create a CommandLineRunner to populate the database.
|
|
26
|
+The default list of beers is created by a `BeerCommandLineRunner` class:
|
82
|
27
|
|
83
|
28
|
```java
|
84
|
|
-package com.example.beer;
|
85
|
|
-
|
86
|
|
-import org.springframework.boot.CommandLineRunner;
|
87
|
|
-import org.springframework.stereotype.Component;
|
88
|
|
-
|
89
|
|
-import java.util.stream.Stream;
|
90
|
|
-
|
91
|
29
|
@Component
|
92
|
30
|
class BeerCommandLineRunner implements CommandLineRunner {
|
93
|
31
|
private final BeerRepository repository;
|
|
@@ -98,29 +36,26 @@ class BeerCommandLineRunner implements CommandLineRunner {
|
98
|
36
|
|
99
|
37
|
@Override
|
100
|
38
|
public void run(String... strings) throws Exception {
|
101
|
|
- // top 5 beers from https://www.beeradvocate.com/lists/top/
|
102
|
|
- Stream.of("Good Morning", "Kentucky Brunch Brand Stout", "ManBearPig", "King Julius",
|
103
|
|
- "Very Hazy", "Budweiser", "Coors Light", "PBR").forEach(name ->
|
|
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 ->
|
104
|
42
|
repository.save(new Beer(name))
|
105
|
43
|
);
|
106
|
|
- System.out.println(repository.findAll());
|
|
44
|
+ repository.findAll().forEach(System.out::println);
|
107
|
45
|
}
|
108
|
46
|
}
|
109
|
47
|
```
|
110
|
48
|
|
111
|
|
-Create a `BeerController` for your REST API. Add some business logic that results in a `/good-beers` endpoint.
|
|
49
|
+The `BeerRepository` interface is decorated with `@RepositoryRestResource` to expose CRUD endpoints for the `Beer` entity.
|
112
|
50
|
|
113
|
51
|
```java
|
114
|
|
-package com.example.beer;
|
115
|
|
-
|
116
|
|
-import org.springframework.web.bind.annotation.GetMapping;
|
117
|
|
-import org.springframework.web.bind.annotation.RestController;
|
|
52
|
+@RepositoryRestResource
|
|
53
|
+interface BeerRepository extends JpaRepository<Beer, Long> {}
|
|
54
|
+```
|
118
|
55
|
|
119
|
|
-import java.util.Collection;
|
120
|
|
-import java.util.HashMap;
|
121
|
|
-import java.util.Map;
|
122
|
|
-import java.util.stream.Collectors;
|
|
56
|
+The last piece of the API is the `BeerController` that exposes `/good-beers` and specifies cross-origin resource sharing (CORS) settings.
|
123
|
57
|
|
|
58
|
+```java
|
124
|
59
|
@RestController
|
125
|
60
|
public class BeerController {
|
126
|
61
|
private BeerRepository repository;
|
|
@@ -130,6 +65,7 @@ public class BeerController {
|
130
|
65
|
}
|
131
|
66
|
|
132
|
67
|
@GetMapping("/good-beers")
|
|
68
|
+ @CrossOrigin(origins = "http://localhost:4200")
|
133
|
69
|
public Collection<Map<String, String>> goodBeers() {
|
134
|
70
|
|
135
|
71
|
return repository.findAll().stream()
|
|
@@ -148,251 +84,128 @@ public class BeerController {
|
148
|
84
|
!beer.getName().equals("PBR");
|
149
|
85
|
}
|
150
|
86
|
}
|
151
|
|
-
|
152
|
87
|
```
|
153
|
88
|
|
154
|
|
-Access the API using `http localhost:8080/good-beers --auth <user>:<password>`.
|
|
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).
|
155
|
90
|
|
156
|
|
-## Create Ionic App
|
|
91
|
+After the app has started, navigate to <http://localhost:8080/good-beers>. You should see the list of good beers in your browser.
|
157
|
92
|
|
158
|
|
-Install Ionic and Cordova: `yarn global add cordova ionic`
|
|
93
|
+![Good Beers JSON](static/good-beers-json.png)
|
159
|
94
|
|
160
|
|
-From a terminal window, create a new application using the following command:
|
|
95
|
+## Create Ionic App
|
161
|
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
|
162
|
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
|
163
|
108
|
ionic start ionic-beer --v2
|
164
|
109
|
```
|
165
|
110
|
|
166
|
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.
|
167
|
112
|
|
168
|
|
-```
|
|
113
|
+```bash
|
169
|
114
|
cd ionic-beer
|
170
|
115
|
ionic serve
|
171
|
116
|
```
|
172
|
117
|
|
173
|
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.
|
174
|
119
|
|
175
|
|
-## Upgrade to Angular 2.3
|
|
120
|
+![Ionic shell with tabs](static/ionic-tabs.png)
|
176
|
121
|
|
177
|
|
-With Angular versions less than 2.3, you can’t extend components and override their templates. The Ionic pages for Stormpath module uses component extension to override the templates in its pages. Because of this, you have to upgrade your project to use Angular 2.3. The only downside to use Angular 2.3 with Ionic 2.0.0 is that you won’t be able to use the `--prod` build flag when compiling. This is because its compiler does not support Angular 2.3.
|
|
122
|
+## Create a Good Beers UI
|
178
|
123
|
|
179
|
|
-To begin, modify `package.json` so all the `angular` dependencies use version `2.3.1` rather than `2.2.1`.
|
|
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`:
|
180
|
125
|
|
181
|
|
-```json
|
182
|
|
-"dependencies": {
|
183
|
|
- "@angular/common": "2.3.1",
|
184
|
|
- "@angular/compiler": "2.3.1",
|
185
|
|
- "@angular/compiler-cli": "2.3.1",
|
186
|
|
- "@angular/core": "2.3.1",
|
187
|
|
- "@angular/forms": "2.3.1",
|
188
|
|
- "@angular/http": "2.3.1",
|
189
|
|
- "@angular/platform-browser": "2.3.1",
|
190
|
|
- "@angular/platform-browser-dynamic": "2.3.1",
|
191
|
|
- "@angular/platform-server": "2.3.1",
|
|
126
|
+```
|
|
127
|
+beer.html
|
|
128
|
+beer.module.ts
|
|
129
|
+beer.scss
|
|
130
|
+beer.ts
|
192
|
131
|
```
|
193
|
132
|
|
194
|
|
-Run `yarn` to update to these versions.
|
|
133
|
+Open `beer.ts` and change the name of the class to be `BeerPage`.
|
195
|
134
|
|
196
|
|
-## Install Ionic Pages for Stormpath
|
|
135
|
+```typescript
|
|
136
|
+export class BeerPage {
|
197
|
137
|
|
198
|
|
-Install [Ionic pages for Stormpath](https://github.com/stormpath/stormpath-sdk-angular-ionic):
|
|
138
|
+ constructor(public navCtrl: NavController, public navParams: NavParams) {
|
|
139
|
+ }
|
199
|
140
|
|
200
|
|
-```
|
201
|
|
-yarn add angular-stormpath-ionic
|
|
141
|
+ ionViewDidLoad() {
|
|
142
|
+ console.log('ionViewDidLoad BeerPage');
|
|
143
|
+ }
|
|
144
|
+
|
|
145
|
+}
|
202
|
146
|
```
|
203
|
147
|
|
204
|
|
-Modify `src/app/app.module.ts` to define a `stormpathConfig` function. This function is used to configure the `endpointPrefix` to point to your Spring Boot API. Import `StormpathModule`, `StormpathIonicModule`, and override the provider of `StormpathConfiguration`. You’ll also need to append Stormpath's pre-built Ionic pages to `entryComponents`.
|
|
148
|
+Modify `beer.module.ts` to change the class name too.
|
205
|
149
|
|
206
|
150
|
```typescript
|
207
|
|
-import { StormpathConfiguration, StormpathModule } from 'angular-stormpath';
|
208
|
|
-import { StormpathIonicModule, LoginPage, ForgotPasswordPage, RegisterPage } from 'angular-stormpath-ionic';
|
209
|
|
-
|
210
|
|
-export function stormpathConfig(): StormpathConfiguration {
|
211
|
|
- let spConfig: StormpathConfiguration = new StormpathConfiguration();
|
212
|
|
- spConfig.endpointPrefix = 'http://localhost:8080';
|
213
|
|
- return spConfig;
|
214
|
|
-}
|
|
151
|
+import { NgModule } from '@angular/core';
|
|
152
|
+import { IonicModule } from 'ionic-angular';
|
|
153
|
+import { BeerPage } from './beer';
|
215
|
154
|
|
216
|
155
|
@NgModule({
|
217
|
|
- ...
|
218
|
|
- imports: [
|
219
|
|
- IonicModule.forRoot(MyApp),
|
220
|
|
- StormpathModule,
|
221
|
|
- StormpathIonicModule
|
|
156
|
+ declarations: [
|
|
157
|
+ BeerPage
|
222
|
158
|
],
|
223
|
|
- bootstrap: [IonicApp],
|
224
|
|
- entryComponents: [
|
225
|
|
- ...
|
226
|
|
- LoginPage,
|
227
|
|
- ForgotPasswordPage,
|
228
|
|
- RegisterPage
|
|
159
|
+ imports: [
|
|
160
|
+ IonicModule.forRoot(BeerPage),
|
229
|
161
|
],
|
230
|
|
- providers: [
|
231
|
|
- {provide: ErrorHandler, useClass: IonicErrorHandler},
|
232
|
|
- {provide: StormpathConfiguration, useFactory: stormpathConfig}
|
|
162
|
+ exports: [
|
|
163
|
+ BeerPage
|
233
|
164
|
]
|
234
|
165
|
})
|
235
|
|
-export class AppModule {}
|
|
166
|
+export class BeerModule {}
|
236
|
167
|
```
|
237
|
168
|
|
238
|
|
-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.
|
|
169
|
+Add `BeerModule` to the `imports` list in `app.module.ts`.
|
239
|
170
|
|
240
|
171
|
```typescript
|
241
|
|
-import { Component } from '@angular/core';
|
242
|
|
-import { Platform } from 'ionic-angular';
|
243
|
|
-import { StatusBar, Splashscreen } from 'ionic-native';
|
244
|
|
-import { TabsPage } from '../pages/tabs/tabs';
|
245
|
|
-import { Stormpath } from 'angular-stormpath';
|
246
|
|
-import { LoginPage } from 'angular-stormpath-ionic';
|
247
|
|
-
|
248
|
|
-@Component({
|
249
|
|
- templateUrl: 'app.html'
|
250
|
|
-})
|
251
|
|
-export class MyApp {
|
252
|
|
- rootPage;
|
253
|
|
-
|
254
|
|
- constructor(platform: Platform, private stormpath: Stormpath) {
|
255
|
|
- stormpath.user$.subscribe(user => {
|
256
|
|
- if (!user) {
|
257
|
|
- this.rootPage = LoginPage;
|
258
|
|
- } else {
|
259
|
|
- this.rootPage = TabsPage;
|
260
|
|
- }
|
261
|
|
- });
|
262
|
|
-
|
263
|
|
- platform.ready().then(() => {
|
264
|
|
- // Okay, so the platform is ready and our plugins are available.
|
265
|
|
- // Here you can do any higher level native things you might need.
|
266
|
|
- StatusBar.styleDefault();
|
267
|
|
- Splashscreen.hide();
|
268
|
|
- });
|
269
|
|
- }
|
270
|
|
-}
|
271
|
|
-```
|
272
|
|
-
|
273
|
|
-If you run `ionic serve`, you’ll likely see something similar to the following error in your browser’s console.
|
274
|
|
-
|
275
|
|
-```
|
276
|
|
-XMLHttpRequest cannot load http://localhost:8080/me. Response to preflight request
|
277
|
|
-doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on
|
278
|
|
-the requested resource. Origin 'http://localhost:8100 is therefore not allowed access.
|
279
|
|
-The response had HTTP status code 403.
|
280
|
|
-```
|
|
172
|
+import { BeerModule } from '../pages/beer/beer.module';
|
281
|
173
|
|
282
|
|
-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.
|
283
|
|
-
|
284
|
|
-```
|
285
|
|
-stormpath.web.cors.allowed.originUris = http://localhost:8100,file://
|
286
|
|
-```
|
287
|
|
-
|
288
|
|
-Restart Spring Boot and your Ionic app. You should see a login screen when you run `ionic serve`.
|
289
|
|
-
|
290
|
|
-![Stormpath Login for Ionic](./static/ionic-login.png)
|
291
|
|
-
|
292
|
|
-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.
|
293
|
|
-
|
294
|
|
-```html
|
295
|
|
-<ion-header>
|
296
|
|
- <ion-navbar>
|
297
|
|
- <ion-title>Home</ion-title>
|
298
|
|
- <ion-buttons end>
|
299
|
|
- <button ion-button icon-only (click)="logout()">
|
300
|
|
- Logout
|
301
|
|
- </button>
|
302
|
|
- </ion-buttons>
|
303
|
|
- </ion-navbar>
|
304
|
|
-</ion-header>
|
305
|
|
-
|
306
|
|
-<ion-content padding>
|
|
174
|
+@NgModule({
|
307
|
175
|
...
|
308
|
|
- <p *ngIf="(user$ | async)">
|
309
|
|
- You are logged in as: <b>{{ ( user$ | async ).fullName }}</b>
|
310
|
|
- </p>
|
311
|
|
-</ion-content>
|
312
|
|
-```
|
313
|
|
-
|
314
|
|
-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.
|
315
|
|
-
|
316
|
|
-```typescript
|
317
|
|
-import { Account, Stormpath } from 'angular-stormpath';
|
318
|
|
-import { Observable } from 'rxjs';
|
319
|
|
-...
|
320
|
|
-export class HomePage {
|
321
|
|
- user$: Observable<Account | boolean>;
|
322
|
|
-
|
323
|
|
- constructor(private stormpath: Stormpath) {
|
324
|
|
- this.user$ = this.stormpath.user$;
|
325
|
|
- }
|
326
|
|
-
|
327
|
|
- logout(): void {
|
328
|
|
- this.stormpath.logout();
|
329
|
|
- }
|
330
|
|
-}
|
331
|
|
-```
|
332
|
|
-
|
333
|
|
-If you’re logged in, you should see a screen with a logout button and the name of the currently logged in user.
|
334
|
|
-
|
335
|
|
-![Logged in as: Hip User](./static/ionic-home.png)
|
336
|
|
-
|
337
|
|
-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.
|
338
|
|
-
|
339
|
|
-```xml
|
340
|
|
-<preference name="KeyboardDisplayRequiresUserAction" value="false"/>
|
341
|
|
-```
|
342
|
|
-
|
343
|
|
-Check your changes into Git.
|
344
|
|
-
|
345
|
|
-```
|
346
|
|
-git add .
|
347
|
|
-git commit -m "Add Stormpath"
|
|
176
|
+ imports: [
|
|
177
|
+ BrowserModule,
|
|
178
|
+ IonicModule.forRoot(MyApp),
|
|
179
|
+ BeerModule
|
|
180
|
+ ],
|
|
181
|
+ ...
|
|
182
|
+})
|
348
|
183
|
```
|
349
|
184
|
|
350
|
|
-## Build a Good Beers UI
|
351
|
|
-
|
352
|
|
-Run `ionic generate page beer` to create a component and a template to display the list of good beers.
|
|
185
|
+Run `ionic g provider beer-service` to create a service to fetch the beer list from the Spring Boot API.
|
353
|
186
|
|
354
|
|
-Add `BeerPage` to the `declarations` and `entryComponent` lists in `app.module.ts`.
|
355
|
|
-
|
356
|
|
-Run `ionic generate provider beer-service` to create a service to fetch the beer list from the Spring Boot API.
|
357
|
|
-
|
358
|
|
-Change `src/providers/beer-service.ts` to use have a `getGoodBeers()` method.
|
|
187
|
+Change `src/providers/beer-service.ts` to have constants for the API path and add a `getGoodBeers()` method.
|
359
|
188
|
|
360
|
189
|
```typescript
|
361
|
190
|
import { Injectable } from '@angular/core';
|
362
|
|
-import { Http, Response, RequestOptions } from '@angular/http';
|
|
191
|
+import { Http, Response } from '@angular/http';
|
363
|
192
|
import 'rxjs/add/operator/map';
|
364
|
193
|
import { Observable } from 'rxjs';
|
365
|
|
-import { StormpathConfiguration } from 'angular-stormpath';
|
366
|
194
|
|
367
|
195
|
@Injectable()
|
368
|
196
|
export class BeerService {
|
369
|
|
- public API;
|
370
|
|
- public BEER_API;
|
|
197
|
+ public API = 'http://localhost:8080';
|
|
198
|
+ public BEER_API = this.API + '/beers';
|
371
|
199
|
|
372
|
|
- constructor(public http: Http, public config: StormpathConfiguration) {
|
373
|
|
- this.API = config.endpointPrefix;
|
374
|
|
- this.BEER_API = this.API + '/beers';
|
375
|
|
- }
|
|
200
|
+ constructor(private http: Http) {}
|
376
|
201
|
|
377
|
202
|
getGoodBeers(): Observable<any> {
|
378
|
|
- let options = new RequestOptions({ withCredentials: true });
|
379
|
|
- return this.http.get(this.API + '/good-beers', options)
|
|
203
|
+ return this.http.get(this.API + '/good-beers')
|
380
|
204
|
.map((response: Response) => response.json());
|
381
|
205
|
}
|
382
|
206
|
}
|
383
|
207
|
```
|
384
|
208
|
|
385
|
|
-**TIP:** If you don’t want to pass in `withCredentials: true`, you can add the API URI as an `autoAuthorizeUri` in `StormpathConfiguration`.
|
386
|
|
-
|
387
|
|
-```typescript
|
388
|
|
-export function stormpathConfig(): StormpathConfiguration {
|
389
|
|
- let spConfig: StormpathConfiguration = new StormpathConfiguration();
|
390
|
|
- spConfig.endpointPrefix = 'http://localhost:8080';
|
391
|
|
- spConfig.autoAuthorizedUris.push(new RegExp(spConfig.endpointPrefix + '/*'));
|
392
|
|
- return spConfig;
|
393
|
|
-}
|
394
|
|
-```
|
395
|
|
-
|
396
|
209
|
Modify `beer.html` to show the list of beers.
|
397
|
210
|
|
398
|
211
|
```html
|
|
@@ -405,28 +218,43 @@ Modify `beer.html` to show the list of beers.
|
405
|
218
|
|
406
|
219
|
<ion-content padding>
|
407
|
220
|
<ion-list>
|
408
|
|
- <ion-item *ngFor="let beer of beers" >
|
|
221
|
+ <ion-item *ngFor="let beer of beers">
|
409
|
222
|
<h2>{{beer.name}}</h2>
|
410
|
223
|
</ion-item>
|
411
|
224
|
</ion-list>
|
412
|
225
|
</ion-content>
|
413
|
226
|
```
|
414
|
227
|
|
415
|
|
-Update `beer.ts` to import `BeerService` and add as a provider. Call the `getGoodBeers()` method in the `ionViewDidLoad()` lifecycle method.
|
|
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.
|
416
|
242
|
|
417
|
243
|
```typescript
|
418
|
244
|
import { Component } from '@angular/core';
|
|
245
|
+import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
419
|
246
|
import { BeerService } from '../../providers/beer-service';
|
420
|
247
|
|
|
248
|
+@IonicPage()
|
421
|
249
|
@Component({
|
422
|
250
|
selector: 'page-beer',
|
423
|
|
- templateUrl: 'beer.html',
|
424
|
|
- providers: [BeerService]
|
|
251
|
+ templateUrl: 'beer.html'
|
425
|
252
|
})
|
426
|
253
|
export class BeerPage {
|
427
|
254
|
private beers: Array<any>;
|
428
|
255
|
|
429
|
|
- constructor(public beerService: BeerService) {
|
|
256
|
+ constructor(public navCtrl: NavController, public navParams: NavParams,
|
|
257
|
+ public beerService: BeerService) {
|
430
|
258
|
}
|
431
|
259
|
|
432
|
260
|
ionViewDidLoad() {
|
|
@@ -451,19 +279,16 @@ import { BeerPage } from '../beer/beer';
|
451
|
279
|
templateUrl: 'tabs.html'
|
452
|
280
|
})
|
453
|
281
|
export class TabsPage {
|
454
|
|
- // this tells the tabs component which Pages
|
455
|
|
- // should be each tab's root Page
|
456
|
282
|
tab1Root: any = HomePage;
|
457
|
283
|
tab2Root: any = BeerPage;
|
458
|
284
|
tab3Root: any = ContactPage;
|
459
|
285
|
tab4Root: any = AboutPage;
|
460
|
286
|
|
461
|
|
- constructor() {
|
462
|
|
- }
|
|
287
|
+ constructor() {}
|
463
|
288
|
}
|
464
|
289
|
```
|
465
|
290
|
|
466
|
|
-Update `tabs.html` too!
|
|
291
|
+You'll also need to update `tabs.html` to have the new tab order.
|
467
|
292
|
|
468
|
293
|
```html
|
469
|
294
|
<ion-tabs>
|
|
@@ -474,7 +299,9 @@ Update `tabs.html` too!
|
474
|
299
|
</ion-tabs>
|
475
|
300
|
```
|
476
|
301
|
|
477
|
|
-Add some fun with Giphy! Run `ionic generate provider giphy-service`. Replace the code in `src/providers/giphy-service.ts` with the following TypeScript:
|
|
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:
|
478
|
305
|
|
479
|
306
|
```typescript
|
480
|
307
|
import { Injectable } from '@angular/core';
|
|
@@ -485,6 +312,7 @@ import { Observable } from 'rxjs';
|
485
|
312
|
// http://tutorials.pluralsight.com/front-end-javascript/getting-started-with-angular-2-by-building-a-giphy-search-application
|
486
|
313
|
export class GiphyService {
|
487
|
314
|
|
|
315
|
+ // Public beta key: https://github.com/Giphy/GiphyAPI#public-beta-key
|
488
|
316
|
giphyApi = 'https://api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&q=';
|
489
|
317
|
|
490
|
318
|
constructor(public http: Http) {
|
|
@@ -504,22 +332,38 @@ export class GiphyService {
|
504
|
332
|
}
|
505
|
333
|
```
|
506
|
334
|
|
507
|
|
-Update `beer.ts` to take advantage of `GiphyService`:
|
|
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.
|
508
|
350
|
|
509
|
351
|
```typescript
|
510
|
352
|
import { Component } from '@angular/core';
|
|
353
|
+import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
511
|
354
|
import { BeerService } from '../../providers/beer-service';
|
512
|
355
|
import { GiphyService } from '../../providers/giphy-service';
|
513
|
356
|
|
|
357
|
+@IonicPage()
|
514
|
358
|
@Component({
|
515
|
359
|
selector: 'page-beer',
|
516
|
|
- templateUrl: 'beer.html',
|
517
|
|
- providers: [BeerService, GiphyService]
|
|
360
|
+ templateUrl: 'beer.html'
|
518
|
361
|
})
|
519
|
362
|
export class BeerPage {
|
520
|
363
|
private beers: Array<any>;
|
521
|
364
|
|
522
|
|
- constructor(public beerService: BeerService, public giphyService: GiphyService) {
|
|
365
|
+ constructor(public navCtrl: NavController, public navParams: NavParams,
|
|
366
|
+ public beerService: BeerService, public giphyService: GiphyService) {
|
523
|
367
|
}
|
524
|
368
|
|
525
|
369
|
ionViewDidLoad() {
|
|
@@ -546,11 +390,49 @@ Update `beer.html` to display the image retrieved:
|
546
|
390
|
</ion-item>
|
547
|
391
|
```
|
548
|
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
|
+
|
549
|
433
|
If everything works as expected, you should see a page similar to the one below in your browser.
|
550
|
434
|
|
551
|
|
-<p align="center">
|
552
|
|
-<img src="./static/good-beers-ui.png" width="600" alt="Good Beers UI">
|
553
|
|
-</p>
|
|
435
|
+![Good Beers UI](static/good-beers-ui.png)
|
554
|
436
|
|
555
|
437
|
### Add a Modal for Editing
|
556
|
438
|
|
|
@@ -578,12 +460,13 @@ In this same file, change `<ion-item>` to have a click handler for opening the m
|
578
|
460
|
Add `ModalController` as a dependency in `BeerPage` and add an `openModal()` method.
|
579
|
461
|
|
580
|
462
|
```typescript
|
581
|
|
-import { ModalController } from 'ionic-angular';
|
|
463
|
+import { IonicPage, ModalController, NavController, NavParams } from 'ionic-angular';
|
582
|
464
|
|
583
|
465
|
export class BeerPage {
|
584
|
466
|
private beers: Array<any>;
|
585
|
467
|
|
586
|
|
- constructor(public beerService: BeerService, public giphyService: GiphyService,
|
|
468
|
+ constructor(public navCtrl: NavController, public navParams: NavParams,
|
|
469
|
+ public beerService: BeerService, public giphyService: GiphyService,
|
587
|
470
|
public modalCtrl: ModalController) {
|
588
|
471
|
}
|
589
|
472
|
|
|
@@ -654,7 +537,7 @@ export class BeerModalPage {
|
654
|
537
|
}
|
655
|
538
|
```
|
656
|
539
|
|
657
|
|
-Create `beer-modal.html` as a template for this page.
|
|
540
|
+Add the import for `BeerModalPage` to `beer.ts`, then create `beer-modal.html` as a template for this page.
|
658
|
541
|
|
659
|
542
|
```html
|
660
|
543
|
<ion-header>
|
|
@@ -699,7 +582,7 @@ Create `beer-modal.html` as a template for this page.
|
699
|
582
|
</ion-content>
|
700
|
583
|
```
|
701
|
584
|
|
702
|
|
-Add `BeerModalPage` to the `declarations` and `entryComponent` lists in `app.module.ts`.
|
|
585
|
+Add `BeerModalPage` to the `declarations` and `entryComponent` lists in `beer.module.ts`.
|
703
|
586
|
|
704
|
587
|
You'll also need to modify `beer-service.ts` to have `get()` and `save()` methods.
|
705
|
588
|
|
|
@@ -721,6 +604,40 @@ save(beer: any): Observable<any> {
|
721
|
604
|
}
|
722
|
605
|
```
|
723
|
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
|
+
|
724
|
641
|
### Add Swipe to Delete
|
725
|
642
|
|
726
|
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>`.
|
|
@@ -766,7 +683,8 @@ remove(beer) {
|
766
|
683
|
Add `toastCtrl` as a dependency in the constructor so everything compiles.
|
767
|
684
|
|
768
|
685
|
```typescript
|
769
|
|
-constructor(public beerService: BeerService, public giphyService: GiphyService,
|
|
686
|
+constructor(public navCtrl: NavController, public navParams: NavParams,
|
|
687
|
+ public beerService: BeerService, public giphyService: GiphyService,
|
770
|
688
|
public modalCtrl: ModalController, public toastCtrl: ToastController) {
|
771
|
689
|
}
|
772
|
690
|
```
|
|
@@ -780,37 +698,36 @@ remove(id: string) {
|
780
|
698
|
}
|
781
|
699
|
```
|
782
|
700
|
|
783
|
|
-After making these additions, you should be able to add, edit and delete beers.
|
|
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.
|
784
|
702
|
|
785
|
|
-<p align="center">
|
786
|
|
-<img src="./static/beer-modal.png" width="350">
|
787
|
|
-<img src="./static/beer-delete.png" width="350">
|
788
|
|
-</p>
|
|
703
|
+![Left swipe](static/beer-delete.png)
|
789
|
704
|
|
790
|
705
|
## PWAs with Ionic
|
791
|
706
|
|
792
|
|
-Ionic 2 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/).
|
|
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).
|
793
|
710
|
|
794
|
|
-If you run the [Lighthouse Chrome extension](https://developers.google.com/web/tools/lighthouse/) on this application, you’ll get a mediocre score (54/100).
|
|
711
|
+![Lighthouse: 51](static/lighthouse-51.png)
|
795
|
712
|
|
796
|
|
-To register a service worker, and improve the app’s score, uncomment the following block in `index.html`.
|
|
713
|
+To register a service worker, and improve the app’s score, uncomment the following block in `src/index.html`.
|
797
|
714
|
|
798
|
715
|
```html
|
799
|
716
|
<!-- un-comment this code to enable service worker
|
800
|
717
|
<script>
|
801
|
|
- if ('serviceWorker' in navigator) {
|
802
|
|
- navigator.serviceWorker.register('service-worker.js')
|
803
|
|
- .then(() => console.log('service worker installed'))
|
804
|
|
- .catch(err => console.log('Error', err));
|
805
|
|
- }
|
|
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
|
+}
|
806
|
723
|
</script>-->
|
807
|
724
|
```
|
808
|
725
|
|
809
|
|
-After making this change, the score should improve. In my tests, it increased to 69/100. The remaining issues were:
|
|
726
|
+After making this change, the score should improve. In my tests, it increased to 66/100. The remaining issues were:
|
810
|
727
|
|
811
|
|
-* The page body should render some content if its scripts are not available. This could likely be solved with [Angular’s app-shell directives](https://www.npmjs.com/package/@angular/app-shell).
|
812
|
|
-* Site is not on HTTPS and does not redirect HTTP to HTTPS.
|
813
|
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.
|
814
|
731
|
|
815
|
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.
|
816
|
733
|
|
|
@@ -819,7 +736,7 @@ chrome://flags/#bypass-app-banner-engagement-checks
|
819
|
736
|
chrome://flags/#enable-add-to-shelf
|
820
|
737
|
```
|
821
|
738
|
|
822
|
|
-After enabling these flags, you’ll see an error in your browser’s console about `assets/imgs/logo.png` not being found. This files is referenced in `src/manifest.json`. You can fix this by copying a 512x512 PNG into this location or by modifying `manifest.json` accordingly.
|
|
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.
|
823
|
740
|
|
824
|
741
|
## Deploy to a Mobile Device
|
825
|
742
|
|
|
@@ -827,6 +744,8 @@ It’s pretty cool that you’re able to develop mobile apps with Ionic in your
|
827
|
744
|
|
828
|
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.
|
829
|
746
|
|
|
747
|
+![Ionic Labs](static/ionic-labs.png)
|
|
748
|
+
|
830
|
749
|
### iOS
|
831
|
750
|
|
832
|
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.
|
|
@@ -854,7 +773,7 @@ open ionic-beer.xcodeproj
|
854
|
773
|
|
855
|
774
|
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.
|
856
|
775
|
|
857
|
|
-Deploying to your phone will likely fail because it won't be able to connect to `http://localhost:8080`. To fix this, copy [this script](./deploy.sh) to your hard drive. It expects to be in a directory above your apps. It also expects your apps to be named `client` and `server`.
|
|
776
|
+Deploying to your phone will likely fail because it won't be able to connect to `http://localhost:8080`. To fix this, copy [this script](./deploy.sh) to your hard drive. It expects to be in a directory above your apps. It also expects your apps to be named `ionic-beer` and `server`.
|
858
|
777
|
|
859
|
778
|
If you don't have a Cloud Foundry account, you'll need to [create one](https://account.run.pivotal.io/z/uaa/sign-up) and install its command line tools for this script to work.
|
860
|
779
|
|
|
@@ -862,13 +781,9 @@ If you don't have a Cloud Foundry account, you'll need to [create one](https://a
|
862
|
781
|
brew tap cloudfoundry/tap && brew install cf-cli
|
863
|
782
|
```
|
864
|
783
|
|
865
|
|
-Once you’re configured your phone, computer, and Apple ID to work, you should be able to open the app and see all the screens you created. Below are the ones the ones I captured on my iPhone 6s Plus.
|
|
784
|
+Once you’re 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.
|
866
|
785
|
|
867
|
|
-<p align="center">
|
868
|
|
-<img src="./static/iphone-login.png" width="250">
|
869
|
|
-<img src="./static/iphone-register.png" width="250">
|
870
|
|
-<img src="./static/iphone-forgot-password.png" width="250">
|
871
|
|
-</p>
|
|
786
|
+![iPhone Beer List](static/iphone-beer-list.png)
|
872
|
787
|
|
873
|
788
|
### Android
|
874
|
789
|
|
|
@@ -903,11 +818,18 @@ Skin: Skin with dynamic hardware controls
|
903
|
818
|
|
904
|
819
|
After performing these steps, you should be able to run `ionic emulate android` and see your app running in the AVD.
|
905
|
820
|
|
|
821
|
+![Android Beer List](static/android-beer-list.png)
|
|
822
|
+
|
906
|
823
|
## Learn More
|
907
|
|
-I hope you’ve enjoyed this tour of Ionic, Angular, and Stormpath. I like how Ionic takes your web development skills up a notch and allows you to create mobile applications that look and behave natively.
|
908
|
824
|
|
909
|
|
-To learn more about Ionic, Angular, or Stormpath, please see the following resources:
|
|
825
|
+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.
|
|
826
|
+
|
|
827
|
+You can find a completed version of the application created in this blog post [on GitHub](https://github.com/oktadeveloper/spring-boot-ionic-example).
|
|
828
|
+
|
|
829
|
+If you encountered issues, please [create an issue in GitHub](TODO) or hit me up on Twitter [@mraible](https://twitter.com/mraible).
|
|
830
|
+
|
|
831
|
+To learn more about Ionic or Angular, please see the following resources:
|
910
|
832
|
|
911
|
833
|
* [Get started with Ionic Framework](http://ionicframework.com/getting-started/)
|
|
834
|
+* [Angular Authentication with OpenID Connect and Okta in 20 Minutes](http://developer.okta.com/blog/2017/04/17/angular-authentication-with-oidc)
|
912
|
835
|
* [Getting Started with Angular](https://www.youtube.com/watch?v=Jq3szz2KOOs) A YouTube webinar by yours truly. ;)
|
913
|
|
-* [Stormpath Client API Guide](https://docs.stormpath.com/client-api/product-guide/latest/)
|