Benutzer:MovGP0/Angular/Redux
Zur Navigation springen
Zur Suche springen
MovGP0 | Über mich | Hilfen | Artikel | Weblinks | Literatur | Zitate | Notizen | Programmierung | MSCert | Physik |
|
Redux
[Bearbeiten | Quelltext bearbeiten]Redux mit Angular (NgRedux)
[Bearbeiten | Quelltext bearbeiten]- Code based on [1].
npm install ng2-redux
- app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { CourseComponent } from './courses/course.component';
import { CourseService } from './courses/course.service';
import { CourseListComponent } from './courses/course-list.component';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryStoryService } from '../api/in-memory-story.service';
import { AppRoutingModule } from './app-routing.module';
import { RouterModule } from '@angular/router';
import { FilterTextComponent, FilterService } from './blocks/filter-text';
import { ToastComponent, ToastService } from './blocks/toast';
import { SpinnerComponent, SpinnerService } from './blocks/spinner';
import { ModalComponent, ModalService } from './blocks/modal';
import { ExceptionService } from './blocks/exception.service';
import { NgReduxModule, NgReduxModule } from 'ng2-redux';
import { store, IAppState } from './store';
import { CourseActions } from './courses/course.actions';
@NgModule({
declarations: [
AppComponent,
CourseComponent,
CourseListComponent,
FilterTextComponent,
ToastComponent,
SpinnerComponent,
ModalComponent,
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryStoryService, { delay: 500 }),
AppRoutingModule,
NgReduxModule
],
providers: [
CourseService,
FilterService,
ToastService,
SpinnerService,
ModalService,
ExceptionService,
CourseActions
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(ngRedux: NgRedux<IAppState>){
ngRedux.provideStore(store);
}
}
- store/IAppState.ts
import { Course } from '../courses/course';
export interface IAppState{
courses: Course[],
filteredCourses: Course[]
}
- store/reducer.ts
import { Course } frem '../courses/course';
import { IAppState } from './IAppState';
import { FILTER_COURSES } from './courses/course.actions';
const courses = [];
const initialState: IAppState = {
courses, // all courses
filteredCourses: courses // initially unfiltered
};
function filterCourses(state, action): IAppState{
// create immutable object
return Object.assign({}, state, {
filteredCourses: state.courses.filter(c => c.name.toLowerCase().indexOf(action.searchText.toLowerCase()) > -1),
});
}
// gets called when courses are returned from the service
function storeCourses(state, action): IAppState{
return Object.assign({}, state, {
courses: action.courses,
filteredCourses: action.courses,
});
}
export function reducer(state = initialState, action){
switch(action.type){
case FILTER_COURSES:
return filterCourses(state, action);
case REQUEST_COURSES_SUCCESS:
return storeCourses(state, action);
default:
return state;
}
}
- store/store.ts
import { createStore } from 'redux';
import { reducer } from './reducer';
import { IAppState } from './IAppState';
// check if dev tools are installed
declare var window:any;
const devToolsExtension: GenericStoreEnhancer = window.devToolsExtension
? window.dexToolsExtension()
: (f) => f;
// create an Redux store
export const store = createStore<IAppState>(reducer);
- store/index.ts
export * from './store';
export * from './IAppState';
export * from './actions';
- courses/actions.ts
import { Injectable } from '@angular/core';
import { NgRedux } from 'ng2-redux';
import { IAppState } from '../store';
import { CourseService } from '../courses/course.service.ts';
export const FILTER_COURSES = 'courses/FILTER';
export const REQUEST_COURSES_SUCCESS = 'courses/REQUEST_SUCCESS';
@Injectable
export class CourseActions {
constructor(private ngRedux: NgRedux<IAppState>, private courseService: CourseService){
}
getCourses(){
this.courseService.getCourses().subsribe(courses => {
this.ngRedux.dispatch({
type: REQUEST_COURSES_SUCCESS,
searchText,
})
});
}
function filterCourses(searchText:string){
this.ngRedux.dispatch({
type: FILTER_COURSES,
searchText,
});
}
}
- courses/course-list.compontent.ts
import { Component, OnInit } from '@angular/core';
import { Course } from './course';
import { FilterTextComponent } from '../blocks/filter-text';
import { IAppState } from '../store';
import { NgRedux, select } from 'ng2-redux';
import { Observable } from 'rxjs/Observable';
import { CourseActions } from './course.actions';
@Component({
selector: 'app-course-list',
templateUrl: './course-list.component.html',
styleUrls: ['./course-list.component.css']
})
export class CourseListComponent implements OnInit {
@select('filteredCourses') filteredCourses$ : Observable<Course>; // $ signifies Observable
constructor(private ngRedux: NgRedux<IAppState>, private courseActions: CourseActions) {
}
filterChanged(searchText: string) {
console.log('user searched: ', searchText);
this.courseActions.filterCourses(searchText);
}
updateFromState(){
this.filteredCourses = store.getState().filteredCourses;
}
ngOnInit() {
this.courseActions.getCourses();
componentHandler.upgradeDom();
}
}
- courses/course-list.compontent.html
<h4>All courses</h4>
<button [routerLink]="['Course', {id: 'new'}]">Add</button>
<filter-text (changed)="filterChanged($event)"></filter-text>
<ul class="courses">
<!-- async is important when subscribing to Observable -->
<li *ngFor="let course of filteredCourses$ | async">
<div>
<div>
<h2>{{course.id}}.{{course.name}}</h2>
</div>
<div>
<button [routerLink]="['Course', {id: course.id}]">
<i>edit</i>
</button>
</div>
</div>
</li>
</ul>
Testing
[Bearbeiten | Quelltext bearbeiten]- reducer.spec.ts
import { reducer } from './reducer';
import { FILTER_COURSES } from '../courses/course.actions';
describe('Reducer', => {
it('should have the correct initial value', () => {
const state = reducer(undefined, {});
expect(state.courses.length).toBe(0);
expect(state.filteredCourses.length).toBe(0);
});
describe('filterCourses', () => {
const courses = [...];
it('should filter out all courses when searchText matches nothing', () => {
const state = { courses, filteredCourses: courses };
const action = {type: FILTER_COURSES, searchText: 'nothing'}
const adaptedState = reducer(state, action);
expect(adaptedState.courses.length).toBe(3);
expect(adaptedState.filteredCourses.length).toBe(0);
});
});
it('should filter out redux course when searchText matches redux', () => {
const state = { courses, filteredCourses: courses };
const action = {type: FILTER_COURSES, searchText: 'redux'}
cons adaptedState = reducer(state, action);
expect(adaptedState.courses.length).toBe(3);
expect(adaptedState.filteredCourses.length).toBe(1);
});
});
});
Enforce Immutability
[Bearbeiten | Quelltext bearbeiten]- use Immutable collections
- use
Object.freeze({ ... })
- use Deep Freeze for freezing nested JSON objects
import { deepFreeze } from 'deepFreeze'; Object.prototype.deepFreeze = deepFreeze; Object.deepFreeze({ ... });
- use Redux Freeze for deep freezing Redux State
import { freeze } from 'redux-freeze'; import { Middleware } from 'redux' // ... constructor(private reduxMiddleware:Middleware){} if (__DEV__) { reduxMiddleware.push(freeze) }
- use Deep Freeze for freezing nested JSON objects
Weblinks
[Bearbeiten | Quelltext bearbeiten]- Redux. Abgerufen am 31. März 2017 (englisch).
- Redux DevTools extension. In: GitHub. Abgerufen am 31. März 2017 (englisch, Browser Erweiterung zur Inspektion des Redux-States).
|}