Skip to content

Latest commit

 

History

History
475 lines (353 loc) · 10.1 KB

step-6.md

File metadata and controls

475 lines (353 loc) · 10.1 KB

API

[Début de la branche step-6]

On a maintenant toutes nos fonctionnalités : Create, Select, Update, Delete. Cela reste du local ou du offline. Il est temps de mettre en place un serveur et d'y inclure des requêtes http.

Pour maximiser le temps, on va créer une API avec le module npm JsonPlaceholder .

npm install -g json-serve

Dans notre projet, inclure un nouveau dossier /server au même niveau que /app et y ajouter un fichier json qui sera notre base de données :

src
|
└───app
|
└───server
|
└───db.json

db.json

{
	"todos": []
}

Entrer dans le terminal :

json-server path-of-json

Allez sur localhost:3000/todos et vous avez une API prête à l'emploi.

Get Todos

Générer un service depuis le terminal :

ng g service services/todo-list

Déclarer dans le AppModule ainsi que le dernier module HttpClient d'Angular :

app.module.ts

// [...]
import { HttpClientModule } from  '@angular/common/http';
import { TodoListService } from  './services/todo-list.service';

@NgModule({
	imports: [
		// [...]
		HttpClientModule
	],
	providers: [TodoListService]
})

export  class  TodoListModule { }

services/todo-list.service

import { HttpClient } from  '@angular/common/http';
import { Observable } from  'rxjs/Observable';
import { Todo } from  '@Models/todo';
import { environment } from  '@Env';
  
@Injectable()
export  class  TodoListService {
	constructor(private  http:HttpClient) {}
	getTodos(): Observable<Todo[]> {
		return  this.http.get<Todo[]>(`${environment.apiUrl}/todos`);
	}

}

Plus simplement pour les imports :

tsconfig.json

{
	"compilerOptions": {
	"baseUrl": "./src",
	"paths": {
		"@Services/*": ["app/services/*"],
		"@Env": ["environments/environment.ts"],
		}
	}
}

Dans ce fichier, placer les paramètres d'API comme l'url :

environment.ts

export const environment = {
	production: false,
	apiUrl: 'http://localhost:3000'
};

Ajouter le service dans le component. Et au resolve de la requête, passer InitTodos :

modules/todo-list/components/all-todos/all-todo.component.ts

// [...]
import { TodoListService } from  '@Services/todo-list';

export  class  AllTodosComponent {
	// [...]
	constructor(
	// [...]
		private  todoListService: TodoListService
	) { // [...] }

	ngOnInit(){
	this.todoListService.getTodos()
		.subscribe((todos) => {
			this.store.dispatch(new  TodoListModule.InitTodos(todos));
		});
	}
	
// [...]

Changer l'action de InitTodos en lui ajoutant un payload :

store/actions/todo-list.action.ts

export namespace TodoListModule {
	// [...]
	export class InitTodos {
		readonly  type = ActionTypes.INIT_TODO;
		constructor(public  payload: Todo[]){ }
	}
	// [...]
}

Le reducer doit retourner le payload à la place du mock que l'on peut totalement supprimer /mocks

store/reducers/todo-list.reducer.ts

// [...]

export  function  todosReducer(
	// [...]

	case TodoListModule.ActionTypes.INIT_TODOS :
		return {
			...state,
			data: [
			...action.payload
			]
		};

	// [...]

Introduction des Effects

Les todos sont bien chargées mais avec NGRX il est possible d'aller plus loin en gérant également la partie asynchrone .

Effects est un second module de NGRX. Il vous fournit des listenners d'actions qui peuvent effectuer des fonctions et retourner une nouvelle action.

Avec un Effect, il n'y aura pas une action InitTodos, mais trois :

  • LoadInitTodos : chargera les données de l'API ;
  • SuccessInitTodos : réponse serveur positive ;
  • ErrorInitTodos : réponse serveur négative.

LoadInitTodos => SuccessInitTodos || ErrorInitTodos

Installation :

npm i @NGRX/effects --save ou yarn add @NGRX/effects --dev

Créer un nouveau fichier effect/ :

app
└───modules
└───store
└───actions
└───reducers
└───selectrs
└───effects
└───todo-list.effect.ts

Alias :

tsconfig.json

{
	"compilerOptions": {
	"baseUrl": "./src",
	"paths": {
		"@Effects/*": ["app/store/effects/*"],
		}
	}
}

Ajouter les trois actions pour l'effect : LOAD_INIT_TODOS, SUCCESS_INIT_TODOS, ERROR_INIT_TODOS.

Retirer l'action InitTodos

store/actions/todo-list.action.ts

export  namespace  TodoListModule {
export  enum  ActionTypes {
	// [...]
	LOAD_INIT_TODOS = '[todoList] Load Init Todos',
	SUCCESS_INIT_TODOS = '[todoList] Success Init Todos',
	ERROR_INIT_TODOS = '[todoList] Error Init Todos',
	// a supprimer INIT_TODOS = '[todoList] Init Todos',
}
// [...]
/* A supprimer
export class InitTodos {
	readonly type = ActionTypes.INIT_TODO;
	constructor(public payload: Todo[]){ }
}
*/

export  class  LoadInitTodos {
	readonly  type = ActionTypes.LOAD_INIT_TODOS;
}

export  class  SuccessInitTodos {
	readonly  type = ActionTypes.SUCCESS_INIT_TODOS;
	constructor(payload: todo[]){}
}

export  class  ErrorInitTodos {
	readonly  type = ActionTypes.ERROR_INIT_TODOS
}

// [...]

export  type  Actions = DeleteTodo
	// | InitTodos
	| LoadInitTodos
	| SuccessInitTodos
	| ErrorInitTodos;
}
// [...]

Nous avons deux étapes lors d'un LoadinitTodos, on peut donc faire un switch sur les propriétés dans le reducer et commencer à jouer avec les booléens loading & loaded .

Ce détail permettra de changer votre template lors du chargement des todos en ajoutant un loader et en désactivant les boutons durant la requête de chargement.

store/reducers/todo-list.reducer.ts

// [...]
export  function  todosReducer(
// [...]
case TodoListModule.ActionTypes.LOAD_INIT_TODOS:
	// Passe le loading a true
	return {
		...state,
		loading: true
	};

case TodoListModule.ActionTypes.SUCCESS_INIT_TODOS:
	// Bind state.data avec les todos du server
	// Passe le loaded a true et le loading a false
	return {
		...state,
		loading: false,
		loaded: true,
		data: action.payload
	};

case TodoListModule.ActionTypes.ERROR_INIT_TODOS:
	// Error rend le loading a false
	return {
		...state,
		loading: false
	};

/* A supprimer
case TodoListModule.ActionTypes.INIT_TODOS :
	return {
		...state,
		data: [
		...action.payload
		]
	};
*/
// [...]

Pour écrire notre premier Effect qui est un Observable, on peut utiliser tout ce que peut nous fournir RXJS pour faire du traitement, combiner des states etc... :

store/effects/todo-list.effect.ts

import { Injectable } from  '@angular/core';
import { Actions, Effect, ofType } from  '@NGRX/effects';
import { Observable } from  'rxjs/Observable';
import { catchError, map, switchMap } from  'rxjs/operators';
import { TodoListModule } from  '@Actions/todo-list.action';
import { TodoListService } from  '../../services/todo-list';

@Injectable()
export  class  TodoListEffects {
	// Ecoute les actions passées dans le store
	@Effect() LoadTodos$: Observable<TodoListModule.Actions> = this.actions$
	.pipe(
		// Si l'action est de type 'LOAD_INIT_TODOS', applique la suite sinon ne fait rien
		ofType(TodoListModule.ActionTypes.LOAD_INIT_TODOS),
		
		// l'action du switchMap est l'objet d'action qui est récupérer dans le ofType
		// action = { type: '[todoList] Load Init Todos' }
		switchMap(action  =>  this.todoListService.getTodos())
		// Dans le switchMap, on éxécute le service qui retournera la réponse dans le map suivant
		// todos = Todo[]
		// Il n'y a plus qu'à renvoyer une action SuccessInitTodos avec les todos en params
		map(todos  =>  new TodoListModule.SuccessInitTodos(todos))
		
		// Si le resolve n'a pas abouti, il passe dans la fonction catchError
		// Qui renvoie l'action ErrorInitTodos
		catchError(() =>  new TodoListModule.ErrorInitTodos())
	);

	constructor(
		private  todoListService: TodoListService,
		private  actions$: Actions
	) {}

}

Pour finaliser l'implémentation des effects, déclarer un tableau d'effect dans l'index du store :

// [...]
import { TodoListEffects } from  '@Effects/todo-list.effect';
// [...]
export  const  appEffects = [TodoListEffects];
// [...]

Ajouter deux autres sélecteurs pour le loading et le loaded state :

store/selectors/todo-list.selector.ts

// [...]
export  const  selectTodosLoading$ =
	createSelector(selectTodoListState$,(todos) =>  todos.loading);
	
export  const  selectTodosLoaded$ =
	createSelector(selectTodoListState$,(todos) =>  todos.loaded);

Importer le module EffectsModule dans le module principal en lui passant notre tableau d'effect :

app.module.ts

// [...]

import { EffectsModule } from  '@NGRX/effects';
import { appEffects, getReducers, REDUCER_TOKEN } from  './store';

@NgModule({

// [...]
imports: [
	// [...]
	EffectsModule.forRoot(appEffects),
],
// [...]
export class AppModule { }

modules/todo-list/components/all-todos/all-todo.component.ts

// [...]
import { TodoListService } from  '../services/todo-list';
import { selectTodoListState$, selectTodosLoading$, selectTodos$ } from  '@Selectors/todo-list.selector';

@Component({
template:  `
	<!--
	Ajoutez votre loader
	-->
	`
})

// [...]
export  class  AllTodosComponent {
public  todosLoading: Observable<boolean>;
// [...]

constructor(
// [...]
// a supprimer private todoListService: TodoListService
) {
// [...]
this.todosLoading = store.pipe(select(selectTodosLoading$));
}

ngOnInit(){

/* A supprimer
this.todoListService.getTodos()
	.subscribe((todos) => {
		this.store.dispatch(new TodoListModule.InitTodos(todos));
	});
*/

this.store.dispatch(new  TodoListModule.LoadInitTodos())

}

// [...]

Plus besoin d'avoir le service dans le component. C'est maintenant l'effect qui, via le LoadInitTodos, va utiliser le service getTodos qui dispatchera notre liste de todos via l'action SuccessInitTodos.