Você está na página 1de 17

AUGUST 27, 2017 BY CHRISTOPHER

How to use ngrx/store in Ionic Framework 3


1
SHARES
ShareTweet

Its not that easy to wrap your head around the whole state management of SPAs.
Theres not that much on the web about it & its such a needed concept when your
application grows. If you have a small app that doesnt take advantage of needing
components to talk to each other, then its not the time to start with ngrx/store. In my
applications that I build, when I need to rely on events in Ionic I think its time to look at
state management.

Built with ionic 3.6.0

The setup
1. ionic start bigbeartechNgrx blank
2. cd bigbeartechNgrx
3. npm i --save @ngrx/store
4. ionic serve --lab

When you run all that then you should now have a blank Ionic Framework.

This is what you should see after starting ionic serve


We need to create an action Its what you call when you want something to happen or
change the state in some way.
Create ./src/actions/mealActions.ts

1. import { Action } from "@ngrx/store";


2. import { Meal } from "../reducers/mealReducer";
3.
4. export const ADD_MEAL = "ADD_MEAL";
5. export const DELETE_MEAL = "DELETE_MEAL";
6. export const RESET = "RESET";
7.
8. export class AddMeal implements Action {
9. readonly type = ADD_MEAL;
10. constructor(public payload: Meal) {}
11. }
12.
13. export class DeleteMeal implements Action {
14. readonly type = DELETE_MEAL;
15.
16. constructor(public payload: any) {}
17. }
18.
19. export class ResetMeal implements Action {
20. readonly type = RESET;
21.
22. constructor(public payload: any) {}
23. }
24.
25. export type All = AddMeal | DeleteMeal | ResetMeal;

Now youre going to need something to change the state with..These are called
reducers! They get the payload from the actions, then return the state.

Create ./src/reducers/mealReducer.ts

1. import { ActionReducer, Action } from "@ngrx/store";


2.
3. import * as MealActions from "../actions/mealActions";
4.
5. export type Action = MealActions.All;
6.
7. export interface Meal {
8. id: string;
9. title: string;
10. content: string;
11. }
12.
13. export interface AppState {
14. meals: [Meal];
15. }
16.
17. export function mealReducer(state = [], action) {
18. console.log(action);
19. switch (action.type) {
20. case MealActions.ADD_MEAL:
21. return [...state, ...action.payload];
22.
23. case MealActions.DELETE_MEAL:
24. return state.filter(meal => meal.id !== action.payload.id);
25.
26. case MealActions.RESET:
27. return [];
28.
29. default:
30. return state;
31. }
32. }

Its looking good, but were missing one thing. How is angular going to know anything
about what a reducer is? Ahhhhh.we need to register it in the app.module.ts file in
imports.

Go to ./src/app/app.module.ts

1. import { BrowserModule } from '@angular/platform-browser';


2. import { ErrorHandler, NgModule } from '@angular/core';
3. import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
4. import { SplashScreen } from '@ionic-native/splash-screen';
5. import { StatusBar } from '@ionic-native/status-bar';
6.
7. import { MyApp } from './app.component';
8. import { HomePage } from '../pages/home/home';
9.
10. import { StoreModule } from "@ngrx/store";
11.
12. @NgModule({
13. declarations: [MyApp, HomePage],
14. imports: [
15. BrowserModule,
16. IonicModule.forRoot(MyApp),
17. StoreModule.forRoot({ meals: mealReducer })
18. ],
19. bootstrap: [IonicApp],
20. entryComponents: [MyApp, HomePage],
21. providers: [
22. StatusBar,
23. SplashScreen,
24. { provide: ErrorHandler, useClass: IonicErrorHandler }
25. ]
26. })
27. export class AppModule {}

But wait a min we can clean this up a bit cant we? How about we extract the call to
reducer & create ./src/reducers/reducers.ts
1. import { mealReducer } from "./mealReducer";
2. export const ROOT_REDUCER = {
3. meals: mealReducer
4. };

You dont have to create the file if you dont want to. But I always like to clean things up
a bit & the app.module.ts can get big in some projects if youre not using lazy loading.

Edit ./src/app/app.module.ts

1. import { BrowserModule } from '@angular/platform-browser';


2. import { ErrorHandler, NgModule } from '@angular/core';
3. import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
4. import { SplashScreen } from '@ionic-native/splash-screen';
5. import { StatusBar } from '@ionic-native/status-bar';
6.
7. import { MyApp } from './app.component';
8. import { HomePage } from '../pages/home/home';
9.
10. import { StoreModule } from "@ngrx/store";
11. import { ROOT_REDUCER } from './../reducers/reducers';
12.
13. @NgModule({
14. declarations: [MyApp, HomePage],
15. imports: [
16. BrowserModule,
17. IonicModule.forRoot(MyApp),
18. StoreModule.forRoot(ROOT_REDUCER)
19. ],
20. bootstrap: [IonicApp],
21. entryComponents: [MyApp, HomePage],
22. providers: [
23. StatusBar,
24. SplashScreen,
25. { provide: ErrorHandler, useClass: IonicErrorHandler }
26. ]
27. })
28. export class AppModule {}

Ok there is a lot of boilerplate code here, but its all needed in the long run.

Edit ./src/pages/home-page.ts

1. import { Component } from '@angular/core';


2. import { NavController } from 'ionic-angular';
3.
4. import { Store } from "@ngrx/store";
5. import * as MealActions from "./../../actions/mealActions";
6. import { AppState } from './../../reducers/mealReducer';
7. import { Observable } from "rxjs/Observable";
8.
9. @Component({
10. selector: "page-home",
11. templateUrl: "home.html"
12. })
13. export class HomePage {
14. form: any = {
15. title: "",
16. content: ""
17. };
18. meals: Observable<any>;
19.
20. constructor(public navCtrl: NavController, private store: Store<AppState>) {
21. this.meals = store.select<any>("meals");
22. }
23.
24. addMeal() {
25. let id = Math.random().toString(36).substr(2, 10);
26. this.store.dispatch(
27. new MealActions.AddMeal({
28. id: id,
29. title: this.form.title,
30. content: this.form.content
31. })
32. );
33. }
34.
35. removeMeal(_meal) {
36. this.store.dispatch(new MealActions.DeleteMeal({ id: _meal.id }));
37. }
38.
39. resetMeals() {
40. this.store.dispatch(new MealActions.ResetMeal({}));
41. }
42. }

We use the dispatch on the store with the action that we created
called mealActions.ts .. The action will then send the payload to mealreducer, which
will take the payload and return it back to the state. Best of allits reactive! That means
that its using RxJS under the hood to subscribe & send the data. So, no more
subscribing to ionic events & updating arrays.

We have the backend about done, but what about the Ui? Its coming, dont think too far
in advance, absorb this information because its a lot to understand at first, but its easy
in the end.

Ok lets get down to the nitty gritty with what the user sees!

Ionic Framework UI setup


Edit ./src/pages/home-page.html
1. <ion-header>
2. <ion-navbar>
3. <ion-title>
4. Meals
5. </ion-title>
6. <ion-buttons end>
7. <button ion-button (tap)="addMeal()">Add</button>
8. <button ion-button (tap)="resetMeals()">Reset</button>
9. </ion-buttons>
10. </ion-navbar>
11. </ion-header>
12.
13. <ion-content>
14. <ion-list>
15. <ion-item>
16. <ion-label>Title</ion-label>
17. <ion-input [(ngModel)]="form.title" placeholder="Meal Title"></ion-input>
18. </ion-item>
19. <ion-item>
20. <ion-label>Content</ion-label>
21. <ion-textarea [(ngModel)]="form.content" placeholder="Meal Content"></ion-textarea>
22. </ion-item>
23. <ion-list-header>Meals</ion-list-header>
24. <button ion-item *ngFor="let meal of meals | async" (tap)="removeMeal(meal)">
25. <h2>{{meal.title}}</h2>
26. <p>{{meal.content}}</p>
27. </button>
28. </ion-list>
29.
30.
31. </ion-content>

The form is just using [(ngModel)].which if this was a production app, you would
probably be using the formBuilder. What about the | async on the end? Its just telling
angular that the array is actually an Observable so this data can change.

The user fills out the form data, then they click the Add button, it then sends the click to
addMeal function, which the addMeal function will send an action to the reducer. The
reducer returns the state, then sends the data to store.select, which we tell it to get the
meals. Async now updates the list with a new meal.

This is the way I understand it & see it happening in my mind. It might not be exactly the
way it works. Im sure Im leaving out a lot of things its doing in the background. You
dont need to worry about how it works at first if you dont want to.

Just know that actions > reducers return state > Observables update view.
Hope this helps people & if you did learn a lot please share the post so it helps others.
@ngrx/store seems to have went through a lot of revisions, so all the blog posts out
there where already out of date.

AUGUST 28, 2017 BY CHRISTOPHER

How to use ngrx/store in Ionic Framework with


Laravel
15
SHARES

ShareTweet
In the last blog post I showed you how to use ngrx/store in Ionic Framework. I got a
comment from someone who asked me how to use ngrx/store with a api backend. This
is going to be a real app with a Laravel Backend to manage the data & not a local
storage. This app will be based on meals. I know, I know Why dont you use books or
posts? LOL I just like the meal concept because of our Mealinvy App.

Built with Ionic 3.6.0 & Laravel 5.4

Initial setup for Laravel


The easiest thing to do is follow the installation guide
at https://laravel.com/docs/5.4/installation

Initial setup for Ionic


1. ionic start bigbeartechNgrxApi blank
2. cd bigbeartechNgrxApi
3. npm i --save @ngrx/store
4. npm i --save @ngrx/store-devtools
5. ionic serve --lab

If you notice the extra store-devtools, well this is optional, but it really helps you see a
great overview of the state. You can also time travel in it, which that means you can go
back & forth in the state. You can use devtools on Google Chrome or Firefox.

Google Chrome: Get the extension

Firefox: Get the addon


This is what you should see after starting ionic serve

Setup for backend

1. php artisan make:model Meal -m

This will create a model for Meal & meals migration.

1. <?php
2.
3. use Illuminate\Support\Facades\Schema;
4. use Illuminate\Database\Schema\Blueprint;
5. use Illuminate\Database\Migrations\Migration;
6.
7. class CreateMealsTable extends Migration
8. {
9. /**
10. * Run the migrations.
11. *
12. * @return void
13. */
14. public function up()
15. {
16. Schema::create('meals', function (Blueprint $table) {
17. $table->increments('id');
18. $table->string('title')->index();
19. $table->text('content');
20. $table->timestamps();
21. });
22. }
23.
24. /**
25. * Reverse the migrations.
26. *
27. * @return void
28. */
29. public function down()
30. {
31. Schema::dropIfExists('meals');
32. }
33. }

You will also need to go edit .env database info

1. DB_CONNECTION=mysql
2. DB_HOST=127.0.0.1
3. DB_PORT=3306
4. DB_DATABASE=homestead
5. DB_USERNAME=homestead
6. DB_PASSWORD=secret

Change to your own credentials.

Run migration:

1. php artisan migrate

Oh no you probably got

1. [Illuminate\Database\QueryException]
2. SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes
(SQL: alter table `users` add unique `users_email_unique`(`email
3. `))

Dont worry this is a really simple fix just go


to ./app/providers/AppServiceProvider.php replace everything:

1. <?php
2.
3. namespace App\Providers;
4.
5. use Illuminate\Support\Facades\Schema;
6. use Illuminate\Support\ServiceProvider;
7.
8. class AppServiceProvider extends ServiceProvider
9. {
10. /**
11. * Bootstrap any application services.
12. *
13. * @return void
14. */
15. public function boot()
16. {
17. Schema::defaultStringLength(191);
18. }
19.
20. /**
21. * Register any application services.
22. *
23. * @return void
24. */
25. public function register()
26. {
27. //
28. }
29. }

We need to create the routes for the meals. Go to ./routes/api.php:

1. <?php
2.
3. Route::prefix('v1')->namespace('Api\V1')->group(function() {
4. Route::resource('meals', 'MealController');
5. });

Create the MealController

1. php artisan make:controller MealController

Move MealController to ./app/Http/Controllers/Api/V1/MealController.php

Replace all the contents of the MealController:

1. <?php
2.
3. namespace App\Http\Controllers\Api\V1;
4.
5. use App\Meal;
6. use Illuminate\Http\Request;
7. use App\Http\Controllers\Controller;
8.
9. class MealController extends Controller
10. {
11. /**
12. * Show all the meals
13. *
14. * @return \App\Meal
15. */
16. public function index()
17. {
18. return Meal::all();
19. }
20.
21. /**
22. * Store the meal
23. *
24. * @param Request $request
25. * @return \App\Meal
26. */
27. public function store(Request $request)
28. {
29. $meal = new Meal;
30. $meal->title = $request->input('title');
31. $meal->content = $request->input('content');
32. $meal->save();
33.
34. return $meal;
35. }
36.
37. /**
38. * Delete a certain meal
39. *
40. * @param Meal $meal
41. * @return JSON
42. */
43. public function destroy(Meal $meal)
44. {
45. $meal->delete();
46.
47. return ['success' => true];
48. }
49. }

Really simple API index, store & delete. But are we missing something? When you call a
get to https://meals.dev/api/v1/meals what is it going to do? Well its going to fail
because of No Access-Control-Allow-Origin header is present this looks bad with
all the red in the console, but its actually really simple to fix.

Im going to use barryvdh/laravel-cors package & pull it in with composer. If you dont
have composer setup go to https://getcomposer.org/doc/00-intro.md

Run in you terminal, powershell or cmd inside of your Laravel project folder:

1. composer require barryvdh/laravel-cors

Add the Cors\ServiceProvider to your config/app.php providers array:

1. Barryvdh\Cors\ServiceProvider::class,

Open up ./app/Http/Kernel.php & add:


1. protected $middlewareGroups = [
2. 'web' => [
3. // ...
4. ],
5.
6. 'api' => [
7. // ...
8. \Barryvdh\Cors\HandleCors::class,
9. ],
10. ];

Are you glad were done with the API backend setup! Oh come on Laravel is fun to
work with! LOL!!!

Now, since this continues my last blog post were starting off from where we left off. If
you havent, you can go and complete all the steps at setting up
Ionic https://blog.bigbeartechworld.com/ionic-ngrx-store/

Continue from last post

Since were working with a api now what is the first thing we need? I know its a
provider to keep the logic separate from the pages.

Create MealProvider:

1. ionic g provider Meal

Replace everything in ./src/providers/meal.ts:

1. import { Injectable } from '@angular/core';


2. import { Http } from '@angular/http';
3. import 'rxjs/add/operator/map';
4.
5. /*
6. Generated class for the MealProvider provider.
7.
8. See https://angular.io/docs/ts/latest/guide/dependency-injection.html
9. for more info on providers and Angular DI.
10. */
11. @Injectable()
12. export class MealProvider {
13.
14. constructor(public http: Http) {
15. console.log('Hello MealProvider Provider');
16. }
17.
18. index() {
19. return this.http.get('https://meals.dev/api/v1/meals').map(res => res.json());
20. }
21.
22. store($meal) {
23. return this.http.post('https://meals.dev/api/v1/meals', $meal).map(res => res.json());
24. }
25.
26. delete($mealId) {
27. return this.http.delete('https://meals.dev/api/v1/meals/' + $mealId).map(res => res.json());
28. }
29.
30. }

You need to change https://meals.dev with your own url. Ok, we need to add another
action to store all the meals in the state at launch. If we dont populate the state at
launch, it will be out of sync.

1. import { Action } from "@ngrx/store";


2. import { Meal } from "../reducers/mealReducer";
3.
4. export const STORE_MEALS = "STORE_MEALS";
5. export const ADD_MEAL = "ADD_MEAL";
6. export const DELETE_MEAL = "DELETE_MEAL";
7. export const RESET = "RESET";
8.
9. export class StoreMeals implements Action {
10. readonly type = STORE_MEALS;
11. constructor(public payload: any) {}
12. }
13.
14. export class AddMeal implements Action {
15. readonly type = ADD_MEAL;
16. constructor(public payload: Meal) {}
17. }
18.
19. export class DeleteMeal implements Action {
20. readonly type = DELETE_MEAL;
21.
22. constructor(public payload: any) {}
23. }
24.
25.
26. export type All = StoreMeals | AddMeal | DeleteMeal | ResetMeal;

Adding an action isnt all we need to do is it? No if you remember from the last post
the reducer is what we use to return the current state.

actions > reducers

So lets add another case to the switch.


1. import { ActionReducer, Action } from "@ngrx/store";
2.
3. import * as MealActions from "../actions/mealActions";
4.
5. export type Action = MealActions.All;
6.
7. export interface Meal {
8. id: string;
9. title: string;
10. content: string;
11. created_at: string;
12. updated_at: string;
13. }
14.
15. export interface AppState {
16. meals: [Meal];
17. }
18.
19. export function mealReducer(state = [], action) {
20. console.log(action);
21. switch (action.type) {
22. case MealActions.STORE_MEALS:
23. return action.payload;
24.
25. case MealActions.ADD_MEAL:
26. return [...state, ...action.payload];
27.
28. case MealActions.DELETE_MEAL:
29. return state.filter(meal => meal.id !== action.payload.id);
30.
31. default:
32. return state;
33. }
34. }

Why did I add created_at & updated_at to the Meal interface? Well, Laravel keeps up
with all the dates for you so it should be in the state if you want it.

Also we are using to MealActions.STORE_MEALS in the switch & just returning the
payload.

Go to your ./src/pages/home.ts:

1. import { Component } from "@angular/core";


2. import { NavController } from "ionic-angular";
3.
4. import { Store } from "@ngrx/store";
5. import * as MealActions from "./../../actions/mealActions";
6. import { AppState } from "./../../reducers/mealReducer";
7. import { Observable } from "rxjs/Observable";
8. import { MealProvider } from "./../../providers/meal/meal";
9. @Component({
10. selector: "page-home",
11. templateUrl: "home.html"
12. })
13. export class HomePage {
14. form: any = {
15. title: "",
16. content: ""
17. };
18. meals: Observable<any>;
19.
20. constructor(
21. public navCtrl: NavController,
22. private store: Store<AppState>,
23. private mealProvider: MealProvider
24. ){
25. this.meals = store.select<any>("meals");
26. }
27.
28. ionViewDidLoad() {
29. this.refreshMeals();
30. }
31.
32. refreshMeals() {
33. this.mealProvider.index().subscribe((res: any) => {
34. let meals = res;
35. this.store.dispatch(new MealActions.StoreMeals(meals));
36. }, (error) => {
37. console.error(error);
38. });
39. }
40.
41. addMeal() {
42. let id = Math.random().toString(36).substr(2, 10);
43. this.store.dispatch(
44. new MealActions.AddMeal({
45. id: id,
46. title: this.form.title,
47. content: this.form.content
48. })
49. );
50. }
51.
52. removeMeal(_meal) {
53. this.store.dispatch(new MealActions.DeleteMeal({ id: _meal.id }));
54. }
55. }

Lets refactor the addMeal function to use the api now.

1. import { Component } from "@angular/core";


2. import { NavController } from "ionic-angular";
3.
4. import { Store } from "@ngrx/store";
5. import * as MealActions from "./../../actions/mealActions";
6. import { AppState } from "./../../reducers/mealReducer";
7. import { Observable } from "rxjs/Observable";
8. import { MealProvider } from "./../../providers/meal/meal";
9.
10. @Component({
11. selector: "page-home",
12. templateUrl: "home.html"
13. })
14. export class HomePage {
15. form: any = {
16. title: "",
17. content: ""
18. };
19. meals: Observable<any>;
20.
21. constructor(
22. public navCtrl: NavController,
23. private store: Store<AppState>,
24. private mealProvider: MealProvider
25. ){
26. this.meals = store.select<any>("meals");
27. }
28.
29. ionViewDidLoad() {
30. this.refreshMeals();
31. }
32.
33. refreshMeals() {
34. this.mealProvider.index().subscribe((res: any) => {
35. let meals = res;
36. this.store.dispatch(new MealActions.StoreMeals(meals));
37. }, (error) => {
38. console.error(error);
39. });
40. }
41.
42. addMeal() {
43. this.mealProvider.store(this.form).subscribe((res: any) => {
44. this.store.dispatch(new MealActions.AddMeal(res));
45. }, error => {
46. console.error(error);
47. });
48. }
49.
50. removeMeal(_meal) {
51. this.mealProvider.store(_meal.id).subscribe((res: any) => {
52. this.store.dispatch(new MealActions.DeleteMeal({ id: _meal.id }));
53. }, error => {
54. console.error(error);
55. });
56. }
57. }

We are using the MealProvider on the addMeal & removeMeal functions. Those are
calling the MealActions.AddMeal & MealActions.DeleteMeal which in the reducer is
handling the state, then returning it. If you installed the chrome devtools or Firefox
addon you can see how the state is updating & deleting.

If you have any questionsdont hesitate to comment. As always, if you learned a lot
please share it to your social networks so it can help others as well! Thank YOU!

Você também pode gostar