How to use ngrx/store in Ionic Framework with Laravel

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 don’t 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

ionic start bigbeartechNgrxApi blank
cd bigbeartechNgrxApi
npm i --save @ngrx/store
npm i --save @ngrx/store-devtools
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

You should now see this in Ionic
This is what you should see after starting ionic serve

Setup for backend

php artisan make:model Meal -m

This will create a model for Meal & meals migration.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMealsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('meals', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title')->index();
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('meals');
    }
}

 

You will also need to go edit .env database info

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

Change to your own credentials.

Run migration:

php artisan migrate

Oh no you probably got

[Illuminate\Database\QueryException]
  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
  `))

Don’t worry this is a really simple fix just go to ./app/providers/AppServiceProvider.php replace everything:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Schema::defaultStringLength(191);
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

 

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

<?php

Route::prefix('v1')->namespace('Api\V1')->group(function() {
    Route::resource('meals', 'MealController');
});

Create the MealController

php artisan make:controller MealController

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

Replace all the contents of the MealController:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Meal;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class MealController extends Controller
{
    /**
     * Show all the meals
     *
     * @return \App\Meal
     */
    public function index()
    {
        return Meal::all();
    }

    /**
     * Store the meal
     *
     * @param Request $request
     * @return \App\Meal
     */
    public function store(Request $request)
    {
        $meal = new Meal;
        $meal->title = $request->input('title');
        $meal->content = $request->input('content');
        $meal->save();

        return $meal;
    }

    /**
     * Delete a certain meal
     *
     * @param Meal $meal
     * @return JSON
     */
    public function destroy(Meal $meal)
    {
        $meal->delete();

        return ['success' => true];
    }
}

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 it’s going to fail because of No ‘Access-Control-Allow-Origin’ header is present this looks bad with all the red in the console, but it’s actually really simple to fix.

I’m going to use barryvdh/laravel-cors package & pull it in with composer. If you don’t 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:

composer require barryvdh/laravel-cors

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

Barryvdh\Cors\ServiceProvider::class,

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

protected $middlewareGroups = [
    'web' => [
       // ...
    ],

    'api' => [
        // ...
        \Barryvdh\Cors\HandleCors::class,
    ],
];

Are you glad we’re done with the API backend setup! Oh come on… Laravel is fun to work with!  LOL!!!

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

Continue from last post

 

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

Create MealProvider:

ionic g provider Meal

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

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

/*
  Generated class for the MealProvider provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular DI.
*/
@Injectable()
export class MealProvider {

  constructor(public http: Http) {
    console.log('Hello MealProvider Provider');
  }

  index() {
    return this.http.get('https://meals.dev/api/v1/meals').map(res => res.json());
  }

  store($meal) {
    return this.http.post('https://meals.dev/api/v1/meals', $meal).map(res => res.json());
  }

  delete($mealId) {
    return this.http.delete('https://meals.dev/api/v1/meals/' + $mealId).map(res => res.json());
  }

}

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 don’t populate the state at launch, it will be out of sync.

import { Action } from "@ngrx/store";
import { Meal } from "../reducers/mealReducer";

export const STORE_MEALS = "STORE_MEALS";
export const ADD_MEAL = "ADD_MEAL";
export const DELETE_MEAL = "DELETE_MEAL";
export const RESET = "RESET";

export class StoreMeals implements Action {
  readonly type = STORE_MEALS;
  constructor(public payload: any) {}
}

export class AddMeal implements Action {
  readonly type = ADD_MEAL;
  constructor(public payload: Meal) {}
}

export class DeleteMeal implements Action {
  readonly type = DELETE_MEAL;

  constructor(public payload: any) {}
}


export type All = StoreMeals | AddMeal | DeleteMeal | ResetMeal;

Adding an action isn’t 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.

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

import * as MealActions from "../actions/mealActions";

export type Action = MealActions.All;

export interface Meal {
  id: string;
  title: string;
  content: string;
  created_at: string;
  updated_at: string;
}

export interface AppState {
  meals: [Meal];
}

export function mealReducer(state = [], action) {
  console.log(action);
  switch (action.type) {
    case MealActions.STORE_MEALS:
      return action.payload;

    case MealActions.ADD_MEAL:
      return [...state, ...action.payload];

    case MealActions.DELETE_MEAL:
      return state.filter(meal => meal.id !== action.payload.id);

    default:
      return state;
  }
}

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:

import { Component } from "@angular/core";
import { NavController } from "ionic-angular";

import { Store } from "@ngrx/store";
import * as MealActions from "./../../actions/mealActions";
import { AppState } from "./../../reducers/mealReducer";
import { Observable } from "rxjs/Observable";
import { MealProvider } from "./../../providers/meal/meal";
@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  form: any = {
    title: "",
    content: ""
  };
  meals: Observable<any>;

  constructor(
    public navCtrl: NavController,
    private store: Store<AppState>,
    private mealProvider: MealProvider
  ) {
    this.meals = store.select<any>("meals");
  }

  ionViewDidLoad() {
    this.refreshMeals();
  }

  refreshMeals() {
    this.mealProvider.index().subscribe((res: any) => {
      let meals = res;
      this.store.dispatch(new MealActions.StoreMeals(meals));
    }, (error) => {
      console.error(error);
    });
  }

  addMeal() {
    let id = Math.random().toString(36).substr(2, 10);
    this.store.dispatch(
      new MealActions.AddMeal({
        id: id,
        title: this.form.title,
        content: this.form.content
      })
    );
  }

  removeMeal(_meal) {
    this.store.dispatch(new MealActions.DeleteMeal({ id: _meal.id }));
  }
}

Lets refactor the addMeal function to use the api now.

import { Component } from "@angular/core";
import { NavController } from "ionic-angular";

import { Store } from "@ngrx/store";
import * as MealActions from "./../../actions/mealActions";
import { AppState } from "./../../reducers/mealReducer";
import { Observable } from "rxjs/Observable";
import { MealProvider } from "./../../providers/meal/meal";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  form: any = {
    title: "",
    content: ""
  };
  meals: Observable<any>;

  constructor(
    public navCtrl: NavController,
    private store: Store<AppState>,
    private mealProvider: MealProvider
  ) {
    this.meals = store.select<any>("meals");
  }

  ionViewDidLoad() {
    this.refreshMeals();
  }

  refreshMeals() {
    this.mealProvider.index().subscribe((res: any) => {
      let meals = res;
      this.store.dispatch(new MealActions.StoreMeals(meals));
    }, (error) => {
      console.error(error);
    });
  }

  addMeal() {
    this.mealProvider.store(this.form).subscribe((res: any) => {
      this.store.dispatch(new MealActions.AddMeal(res));
    }, error => {
      console.error(error);
    });
  }

  removeMeal(_meal) {
    this.mealProvider.store(_meal.id).subscribe((res: any) => {
      this.store.dispatch(new MealActions.DeleteMeal({ id: _meal.id }));
    }, error => {
      console.error(error);
    });
  }
}

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 questions…don’t 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!