Ionic @ In3

presents

graphql and state management

The End result

A simple To-do app built with the following:

  • Ionic 4 (beta)
  • Angular 6
  • Apollo Client
  • Loona

loona

Loona is a state management library that's built on top of Apollo Client, a client-side library you use to communicate with a GraphQL service.

What is it?

loona

why should i use it?

State management libraries can help with the following:

  • performance (fewer UI refreshes)
  • organization of data (keeps your data centralized)
  • debugging

loona

The concepts

 

  • Queries - borrowed from GraphQL, queries are used to fetch data from a local or remote source
  • Mutations - also borrowed from GraphQL, mutations are used to modify a remote a local data sotre
  • Store - a single source of truth for your application data
  • Actions - a means of explicitly calling a mutation or another action
  • Updates - modify the store after mutations occur

Getting Started

1. Create your Ionic 4 Project

ionic start ionicLoona --type=angular

Getting Started

2. Install GraphQL and the Apollo Client libraries

npm install graphql graphql-tag apollo-client apollo-angular apollo-angular-link-http apollo-cache apollo-cache-inmemory --save

Getting Started

3. Install the loona libraries

npm install @loona/angular

Getting Started

4. Import ApolloModule into the NgModule of your app.module.ts



//STEP 4 CODE
import {ApolloModule} from "apollo-angular";


@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    
    //STEP 4 CODE
    ApolloModule,
    
    BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Insert the commented code snippets into src/app/app.module.ts

Getting Started

5. Add the LoonaModule and a cache to app.module.ts



//STEP 5 CODE
import { LoonaModule, LOONA_CACHE } from "@loona/angular";
import { InMemoryCache } from "apollo-cache-inmemory";


//STEP 5 CODE
const cache = new InMemoryCache();

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [ApolloModule, 

//STEP 5 CODE
LoonaModule.forRoot(), 

BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [

//STEP 5 CODE
{
    provide : LOONA_CACHE,
    useValue : cache
},

    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Insert the commented code snippets into src/app/app.module.ts

Getting Started

6. Create an Apollo instance and add the LoonaLink to it.



//STEP 6 CODE
import {ApolloModule, Apollo} from "apollo-angular";


@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [ApolloModule, LoonaModule.forRoot(), BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [{
    provide : LOONA_CACHE,
    useValue : cache
  },
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {

  //STEP 6 CODE 
  constuctor(apollo: Apollo, loona: LoonaLink){
    apollo.create({
      link : loona,
      cache
    })
  }
}

Getting Started

7. Define the graphql queries.

import gql from 'graphql-tag';

export const GET_ALL_BOOKS = gql`
        query GetAllBooks {
          books @client {
            id
            title
          }
        }
      `
export const ADD_NEW_BOOK = gql`
      mutation AddNewBook($title: String!) {
        addBook(title: $title) @client {
          id
          title
        }
      }
    `;

export const DELETE_BOOK = gql`
      mutation DeleteBook($id: ID!) {
        deleteBook(id: $id) @client {
          id
        }
      }
    `;

Insert all of the following code in the src/app/queries.ts

Getting Started

8. Define the state & schemas.

import { State, Mutation, Update } from '@loona/angular';
import gql from 'graphql-tag';
import { GET_ALL_BOOKS, ADD_NEW_BOOK } from './queries'

@State({
  typeDefs: `
    type Book {
      id: ID
      title: String
    }

    type Query {
      books: [Book]
    }
  `,
  defaults: {
    books: [{
      id: 100,
      title: "PITA - What Life Is Sometimes",
      __typename: 'Book'
    }],
  },
})
export default class TheState {
  // Resolvers
  // Mutations
  @Mutation('addBook')
  addBook(args, context) {
    // our new book
    const b = {
      id: new Date().getTime(),
      title: args.title,
      __typename: 'Book',
    };
    return b

  }

  @Mutation('deleteBook')
  deleteBook(args, context) {

    context.patchQuery(GET_ALL_BOOKS,
      data => {
        data.books = data.books.filter((b) => {
          return b.id !== args.id
        });
      },
    );

    return { id: args.id, __typename: 'Book', }

  }



  // Updates
  @Update('addBook')
  addToBooks(mutation, context) {

    const book = mutation.result;
    // updates the store
    context.patchQuery(GET_ALL_BOOKS,
      data => {
        data.books = data.books.concat([book]);
      },
    );
  }
  // Action handlers
}

Insert all of the following code in the src/app/app-state.ts

Pass the state defined above to the LoonaModule in src/app/app.module.ts.



//STEP 8 CODE
import State from "./app-state";

const cache = new InMemoryCache();

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [ApolloModule, 
    
    //STEP 8 CODE
    LoonaModule.forRoot([State]), 
    
    BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [{
    provide : LOONA_CACHE,
    useValue : cache
  },
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})

Getting Started

9. Add a component to test the Loona module implementation.

9.a. Delete the contents of the home directory.

9.b. Create a new component called book.component.ts in the src/app directory and post the following content into it.

9.c. In the src/app/app.component.html, replace the router tag with the <book-list tag> so that it looks like this

 

9.d. Add the book component to the declaration list of the src/app/app.module.ts file.

import { Component, Input } from '@angular/core';
import { Loona } from '@loona/angular';

import { map, tap, pluck } from 'rxjs/operators';
import { GET_ALL_BOOKS, ADD_NEW_BOOK, DELETE_BOOK } from './queries'

@Component({
  selector: 'book-list',
  template: `
    <ul>
      <li *ngFor="let book of (books$ | async)">
         {{book.id}} : {{book.title}}
         <button (click)="deleteBook(book.id)">DELETE</button>
      </li>
    </ul>
    <div>
      <input type="text" placeholder=" enter title..." [(ngModel)]="input.title"/>
      <button (click)="addBook()">ADD</button>
    </div>
  `,
  styles: [`h1 { font-family: Lato; }`]
})
export class BookListComponent {

  books$;
  input = {
    title: ""
  }

  constructor(private loona: Loona) { }

  ngOnInit() {
 
    this.books$ = this.loona.query(GET_ALL_BOOKS,
    ).valueChanges.pipe(pluck('data', 'books'));
   
  }

  deleteBook(_id) {
    this.loona.mutate(DELETE_BOOK, { id: _id }).subscribe();
  }

  addBook() {
    this.loona.mutate(ADD_NEW_BOOK, { title: this.input.title }).subscribe();
  }
}
<ion-app>
  <book-list></book-list>
</ion-app>

helpful resources

;