Saltar al contenido

Explora la gestión del estado de la UI con Redux en Angular 4

Aunque la paginación no es estrictamente parte del diseño de la interfaz de usuario de una aplicación, es una parte integral de la interfaz de usuario de la aplicación que puede ser implementada con Redux. El objetivo de implementar la paginación del lado del servidor es utilizar el almacén de la aplicación tanto como sea posible y lograr flexibilidad con el menor código posible.

API de la Bomba Gigante

Para ilustrar cómo funciona la paginación del Redux, usaremos la API de la GiantBomb como fuente de información. Buscaremos los juegos almacenados en la base de datos de la GiantBomb, y luego paginaremos los resultados. La paginación será controlada por el estado de la aplicación.

Explora la gestión del estado de la UI con Redux en Angular 4
Explora la gestión del estado de la UI con Redux en Angular 4

Primero, crea un directorio separado para los juegos:

1$ mkdir src/app/common/games

bash

1 $ touch src/app/common/games.actions.ts

bash

juegos.acciones.ts

12345678910111213141516171819202122232425262728293031import{type}from"../util";import{Action}from"@ngrx/store";exportconstGameActionTypes={/* Como la colección de juegos es asíncrona, es necesario que haya acciones para manejar cada una de las etapas de la solicitud. */LOAD:"[Juegos] cargar juegos",LOAD_SUCCESS:"[Juegos] cargados con éxito",LOAD_FAILURE:"[Juegos] fallaron al cargar juegos"};exportclassLoadGamesActionimplementsAction{type=GameActionTypes.LOAD;constructor(public payload:any){}}exportclassLoadGamesFailedActionimplementsAction{type=GameActionTypes. LOAD_FAILURE;constructor(){}}clase de exportaciónCargaJuegosAcciónAcción exitosaimplementosAcción{type=Tipos de JuegoAcción.LOAD_SUCCESS;constructor(public payload:any){}}tipo de exportaciónJuegosAcción=|CargaJuegosAcción|CargaJuegosFallóAcción|CargaJuegosAcción exitosa;

ts

Redux tiene una convención para cargar resultados asíncronos. Lo hace usando tres acciones: CARGAR, CARGAR_SUCRETO y CARGAR_FALLO. Las dos últimas se envían cuando el middleware resuelve la petición del lado del servidor.

Para averiguar cómo construir el estado de las entidades de los juegos paginados, veamos qué necesita una paginación:

  1. Número de páginas actuales
  2. Cantidad total de artículos
  3. Colección de los artículos actualmente expuestos
  4. (opcional) Número de elementos por página y número de páginas visibles

Teniendo esto en cuenta, así es como debería verse la interfaz de estado:

1234567exporterfaceState{ loaded:boolean; loading:boolean; entities:Array<any;;; count:number; page:number;}

ts

Veamos cómo se ve la implementación completa:

1$ touch src/app/common/games.reducer.ts

bash

juegos.reductor.ts

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768importar{ createSelector }de "reselect";importar*como juegos de"./juegos. actions";exportinterfaceState{ loaded:boolean; loading:boolean; entities:Array<any>; count:number; page:number;}const initialState_State={ loaded:false, loading:false, entities:[], count:0, page:1};exportfunctionreducer(state = initialState, action: games. GameActions):State{switch(action.type){case games.GameActionTypes.LOAD:{const page = action.payload;returnObject.assign({}, state,{ loading:true,/* Si no hay ninguna página seleccionada, utilice la página del estado inicial */ page: page ==null? state. page: page });}case juegos.GameActionTypes.LOAD_SUCCESS:{const juegos = action.payload["resultados"];const juegosCount = action.payload["número_de_total_resultados"];returnObject.assign({}, state,{ loaded:true, loading:false, entities: juegos, count: gamesCount });}case juegos.GameActionTypes.LOAD_FAILURE:{returnObject. assign({}, state,{ loaded:true, loading:false, entidades:[], count:0});}default:return state;}}/* Seleccionadores para el estado que se usará más tarde en el componente de la lista de juegos */exportconstgetEntities=(state:State)=> state. entities;exportconstgetPage=(state:State)=> state.page;exportconstgetCount=(state:State)=> state.count;exportconstgetLoadingState=(state:State)=> state.loading;

ts

Cada vez que se llama a LOAD desde las GamesActions, el número de página está contenido dentro de la carga útil de Action'y luego se asigna al estado. Lo que queda es encontrar una forma de consultar la página del servidor desde el estado de los juegos. Para hacer esto, el estado tiene que ser añadido al almacén de aplicaciones:

index.ts

<pre>123456789101112131415161718192021222324252627importación* como fromJuegos de"./juegos/juegos.reductor";//…exportartinterfazAppState{ disposición: fromLayout. State; juegos: fromGames.State;}exportconst reducers ={ layout: fromLayout.reducer, games: fromGames.reducer};//…/* Seleccionadores de juegos */exportconstgetGamesState=(state:AppState)=> state. juegos;exportconst getGamesEntities =createSelector(getGamesState, fromGames.getEntities);exportconst getGamesCount =createSelector(getGamesState, fromGames. getCount);exportconst getGamesPage =createSelector(getGamesState, fromGames.getPage);exportconst getGamesLoadingState =createSelector( getGamesState, fromGames.getLoadingState);</pre>
ts

getGamesPage se usará para obtener la página actual y enviarla como parámetro en la consulta al servicio.

<pre>1$ touch src/app/common/games.service.ts</pre>
bash

juegos.servicio.ts

<pre>1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556importe{inyectable,inyectable}de"@angular/núcleo& quot;;importar{Respuesta,Http,Encabezados,OpcionesdePedido,Jsonp}de"@angular/http";importar{Almacenar}de"@ngrx/almacenar";importar*como deRoot de". ./index";@Injectable()exportclassGamesService{página pública:número;constructor(privado jsonp:Jsonp,tienda privada:Store<fromRoot. AppState>){/* Obtener la página de los juegos state */ store.select(fromRoot.getGamesPage).subscribe(page=>{this.page= page;});}/* Obtener la lista de juegos. GiantBomb requiere una petición jsnop con un token. ¡Puedes usar esta ficha como regalo de mi parte, el autor, y usarla con moderación! */query(){let pagination =this.paginate(this.page);let url =`http://www.giantbomb.com/api/games/?api_key=b89a6126dc90f68a87a6fe1394e64d7312b242da&?&offset=${ pagination.offset}&limit=${pagination. limit}&format=jsonp&json_callback=JSONP_CALLBACK`;returnthis.jsonp.request(url,{ method:"Get"}).map(res=>{return res["_body"];});} /** Esta función convierte una página en una consulta de paginación. * * @parampage * * @retornos{{{offset: número, límite: número}} */paginate(page:number){let beginItem:number;let endItem:number;// Los elementos por página están codificados, pero puede hacerlos dinámicos añadiendo otro parametraje itemsPerPage_number=10;if(page ==1){ beginItem =0;}else{ beginItem =(page -1)* itemsPerPage;}return{ offset: beginItem, limit: itemsPerPage };}}</pre>
ts

La página seleccionada actualmente se toma del estado y se pasa por la paginación. La paginación es una función de utilidad que convierte la página actual en parámetros de compensación y límite de acuerdo con los requisitos de la API de la GiantBomb para los resultados de la paginación.

A continuación, vamos a implementar el middleware que se utilizará para llamar al servicio y enviar acciones de SUCESO o FALLA.

<pre>1$ touch src/app/common/games.effects.ts</pre>
bash

juegos.efectos.ts

<pre>12345678910111213141516171819202122232425import"rxjs/add/operator/map";import"rxjs/add/operator/catch";import" rxjs/add/operador/interruptorMapa";importar{Observable}de" rxjs/Observable";importar{Inyectable}de" @angular/core"; importar*como juegos de". /juegos. actions";import{Actions,Effect}from"@ngrx/effects";import{GamesService}from"./games.service";import{LoadGamesSuccessAction}from"./games. actions";import{LoadGamesFailedAction}from"./games.actions";@Injectable()exportclassGameEffects{constructor(privado _actions:Actions,privado _service:GamesService){} @Efecto() loadGames$ =esto._acciones.deTipo(juegos.JuegoTiposDeAcción.CARGA).switchMap(()=>esto._servicio.consulta(). map(games=>{returnnewLoadGamesSuccessAction(games);})).catch(()=>Observable.of(newLoadGamesFailedAction());}</pre>
ts

Cada vez que se llama a LOAD desde las GamesActions, el número de página está contenido en la carga útil de Action'y luego se asigna al estado. Lo que queda es encontrar una forma de consultar la página del servidor desde el estado de los juegos. Para hacer esto, el estado tiene que ser añadido al almacén de aplicaciones:

index.ts

<pre>123456789101112131415161718192021222324252627importación* como fromJuegos de"./juegos/juegos.reductor";//…exportartinterfaceAppState{ disposición: fromLayout. State; juegos: fromGames.State;}exportconst reducers ={ layout: fromLayout.reducer, games: fromGames.reducer};//…/* Seleccionadores de juegos */exportconstgetGamesState=(state:AppState)=> state. juegos;exportconst getGamesEntities =createSelector(getGamesState, fromGames.getEntities);exportconst getGamesCount =createSelector(getGamesState, fromGames. getCount);exportconst getGamesPage =createSelector(getGamesState, fromGames.getPage);exportconst getGamesLoadingState =createSelector( getGamesState, fromGames.getLoadingState);</pre>
ts

getGamesPage se usará para obtener la página actual y enviarla como parámetro en la consulta al servicio.

<pre>1$ touch src/app/common/games.service.ts</pre>
bash

juegos.servicio.ts

<pre>1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556importe{inyectable,inyectable}de"@angular/núcleo& quot;;importar{Respuesta,Http,Encabezados,PedirOpciones,Jsonp}de"@angular/http";importar{Almacenar}de"@ngrx/almacenar";importar*como deRoot de". ./index";;@Injectable()exportclassGamesService{página pública:número;constructor(privado jsonp:Jsonp,tienda privada:Store<fromRoot. AppState>){/* Obtener la página de los juegos state */ store.select(fromRoot.getGamesPage).subscribe(page=>{this.page= page;});}/* Obtener la lista de juegos. GiantBomb requiere una petición jsnop con un token. ¡Puedes usar esta ficha como regalo de mi parte, el autor, y usarla con moderación! */query(){let pagination =this.paginate(this.page);let url =`http://www.giantbomb.com/api/games/?api_key=b89a6126dc90f68a87a6fe1394e64d7312b242da&?&offset=${ pagination.offset}&limit=${paginación. limit}&format=jsonp&json_callback=JSONP_CALLBACK`;returnthis.jsonp.request(url,{ method:"Get"}).map(res=>{return res["_body"];});} /** Esta función convierte una página en una consulta de paginación. * * @parampage * * @retornos{{{offset: número, límite: número}} */página(página:número){let beginItem:number;let endItem:number;// Los elementos por página están codificados, pero puede hacerlos dinámicos añadiendo otros elementos de parametrizaciónPerPage: number=10;if(page ==1){ beginItem =0;}else{ beginItem =(page -1)* itemsPerPage;}return{ offset: beginItem, limit: itemsPerPage };}}</pre>
ts

La página seleccionada actualmente se toma del estado y se pasa por la paginación. La paginación es una función de utilidad que convierte la página actual en parámetros de compensación y límite de acuerdo con los requisitos de la API de la GiantBomb para los resultados de la paginación.

A continuación, vamos a implementar el middleware que se utilizará para llamar al servicio y despachar acciones de SUCESO o FALLA.

<pre>1$ touch src/app/common/games.effects.ts</pre>
bash

juegos.efectos.ts

<pre>12345678910111213141516171819202122232425import"rxjs/add/operador/map";import"rxjs/add/operador/catch";import" rxjs/add/operador/interruptorMapa";import{Observable}de" rxjs/Observable";import{Inyectable}de" @angular/core";import*como juegos de". /juegos. actions";import{Actions,Effect}from"@ngrx/effects";import{GamesService}from"./games.service";import{LoadGamesSuccessAction}from". /juegos.acciones";importar{CargarJuegosAcción Fallida}de"./juegos.acciones";@Injectable()exportclassGameEffects{constructor(privado _acciones:Acciones,privado _servicio:GamesService){} @Efecto() loadGames$ =esto._acciones.deTipo(juegos.JuegoTiposDeAcción.CARGA).switchMap(()=>esto._servicio.consulta(). map(games=>{returnnewLoadGamesSuccessAction(games);})).catch(()=>Observable.of(newLoadGamesFailedAction());}</pre>
ts

123456789101112131415161718192021222324252627282930313233343536import*as games from"./common/games/games.actions";//...@Component({ selector: "app-root", templateUrl:"./app.component.html", styleUrls:["./app.component. css"]})exportclassAppComponentientimentsOnInit{//...public games$:Observable<any;;public gamesCount$:Observable<number;;public gamesPage$:Observable<number;;public gamesLoading$:Observable<boolean;;constructor(tienda privada:Store<fromRoot. AppState²;){/* Selecciona todas las partes del estado necesarias para la GamesListComponent */this.games$= store.select(fromRoot.getGamesEntities);this.gamesCount$= store.select(fromRoot.getGamesCount);this.gamesPage$= store.select(fromRoot.getGamesPage);this.gamesLoading$= store. select(fromRoot.getGamesLoadingState);}/* Cuando el componente se inicializa, renderiza la primera página de resultados */ngOnInit(){this.store.dispatch(newgames.LoadGamesAction(1));}///...onGamesPageChanged(page:number){this.store.dispatch(newgames.LoadGamesAction(page));}}

ts

Por último, añade el selector de GamesListComponent a la plantilla de AppComponent: app.component.html

1234&lt;div...-- ... --;&lt;games-list[games]="games$ | async"[count]="gamesCount$ | async"[page]="gamesPage$ | async"[loading]="gamesLoading$ | async"(onPageChanged)="onGamesPageChanged($event)"<;/lista de juegos
html

El tubo de asíncrono utiliza el último valor de los observables, observa los cambios de estado y los pasa como entradas.

Así es como funciona la paginación en acción: