From c39e60ef4376a8ac8160d7bc58b135da081663b1 Mon Sep 17 00:00:00 2001 From: linuxonrails Date: Thu, 15 Dec 2016 10:20:35 +0100 Subject: [PATCH 1/5] Add 17 Observables to the example list --- 17 Observables/package.json | 43 ++ 17 Observables/readme.md | 408 ++++++++++++++++++ 17 Observables/src/actions/index.ts | 4 + 17 Observables/src/actions/memberRequest.ts | 7 + .../src/actions/memberRequestCompleted.ts | 10 + .../src/actions/updateFavouriteColor.ts | 9 + .../src/actions/updateUserProfileName.ts | 8 + 17 Observables/src/app.tsx | 22 + 17 Observables/src/common/actionsEnums.ts | 6 + .../src/components/color/colordisplayer.tsx | 21 + .../color/colordisplayerContainer.ts | 18 + .../src/components/color/colorpicker.tsx | 32 ++ .../components/color/colorpickerContainer.ts | 23 + .../src/components/color/colorslider.tsx | 22 + 17 Observables/src/components/color/index.ts | 7 + .../src/components/helloworld/helloWorld.tsx | 7 + .../helloworld/helloWorldContainer.ts | 18 + .../src/components/helloworld/index.ts | 5 + .../src/components/members/index.ts | 5 + .../components/members/memberAreaContainer.ts | 21 + .../src/components/members/memberarea.tsx | 32 ++ .../src/components/members/memberrow.tsx | 23 + .../src/components/members/membertable.tsx | 37 ++ .../src/components/nameEdit/index.ts | 5 + .../src/components/nameEdit/nameEdit.tsx | 13 + .../components/nameEdit/nameEditContainer.ts | 22 + 17 Observables/src/epics/fetchMembersEpic.ts | 22 + 17 Observables/src/epics/index.ts | 5 + 17 Observables/src/index.html | 12 + 17 Observables/src/main.tsx | 12 + 17 Observables/src/model/color.ts | 5 + 17 Observables/src/model/member.ts | 11 + 17 Observables/src/reducers/index.ts | 8 + 17 Observables/src/reducers/memberReducer.ts | 27 ++ 17 Observables/src/reducers/userProfile.ts | 35 ++ 17 Observables/src/restApi/memberApi.ts | 13 + 17 Observables/src/store.ts | 13 + 17 Observables/tsconfig.json | 16 + 17 Observables/webpack.config.js | 71 +++ 39 files changed, 1078 insertions(+) create mode 100644 17 Observables/package.json create mode 100644 17 Observables/readme.md create mode 100644 17 Observables/src/actions/index.ts create mode 100644 17 Observables/src/actions/memberRequest.ts create mode 100644 17 Observables/src/actions/memberRequestCompleted.ts create mode 100644 17 Observables/src/actions/updateFavouriteColor.ts create mode 100644 17 Observables/src/actions/updateUserProfileName.ts create mode 100644 17 Observables/src/app.tsx create mode 100644 17 Observables/src/common/actionsEnums.ts create mode 100644 17 Observables/src/components/color/colordisplayer.tsx create mode 100644 17 Observables/src/components/color/colordisplayerContainer.ts create mode 100644 17 Observables/src/components/color/colorpicker.tsx create mode 100644 17 Observables/src/components/color/colorpickerContainer.ts create mode 100644 17 Observables/src/components/color/colorslider.tsx create mode 100644 17 Observables/src/components/color/index.ts create mode 100644 17 Observables/src/components/helloworld/helloWorld.tsx create mode 100644 17 Observables/src/components/helloworld/helloWorldContainer.ts create mode 100644 17 Observables/src/components/helloworld/index.ts create mode 100644 17 Observables/src/components/members/index.ts create mode 100644 17 Observables/src/components/members/memberAreaContainer.ts create mode 100644 17 Observables/src/components/members/memberarea.tsx create mode 100644 17 Observables/src/components/members/memberrow.tsx create mode 100644 17 Observables/src/components/members/membertable.tsx create mode 100644 17 Observables/src/components/nameEdit/index.ts create mode 100644 17 Observables/src/components/nameEdit/nameEdit.tsx create mode 100644 17 Observables/src/components/nameEdit/nameEditContainer.ts create mode 100644 17 Observables/src/epics/fetchMembersEpic.ts create mode 100644 17 Observables/src/epics/index.ts create mode 100644 17 Observables/src/index.html create mode 100644 17 Observables/src/main.tsx create mode 100644 17 Observables/src/model/color.ts create mode 100644 17 Observables/src/model/member.ts create mode 100644 17 Observables/src/reducers/index.ts create mode 100644 17 Observables/src/reducers/memberReducer.ts create mode 100644 17 Observables/src/reducers/userProfile.ts create mode 100644 17 Observables/src/restApi/memberApi.ts create mode 100644 17 Observables/src/store.ts create mode 100644 17 Observables/tsconfig.json create mode 100644 17 Observables/webpack.config.js diff --git a/17 Observables/package.json b/17 Observables/package.json new file mode 100644 index 0000000..8ea9780 --- /dev/null +++ b/17 Observables/package.json @@ -0,0 +1,43 @@ +{ + "name": "samplereact", + "version": "1.0.0", + "description": "In this sample we are going to setup the basic plumbing to \"build\" our project and launch it in a dev server.", + "main": "index.js", + "scripts": { + "build": "webpack", + "start": "webpack-dev-server --inline", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@reactivex/rxjs": "^5.0.1", + "@types/rx": "^2.5.34", + "redux-observable": "^0.12.2", + "rx": "^4.1.0", + "rxjs": "^5.0.1", + "typescript": "^2.0.3", + "webpack": "^1.13.2", + "webpack-dev-server": "^1.16.2" + }, + "dependencies": { + "@types/es6-shim": "^0.31.32", + "@types/object-assign": "^4.0.30", + "@types/react": "^0.14.43", + "@types/react-dom": "^0.14.18", + "@types/react-redux": "^4.4.32", + "@types/redux": "^3.6.31", + "bootstrap": "^3.3.7", + "css-loader": "^0.25.0", + "file-loader": "^0.9.0", + "html-webpack-plugin": "^2.22.0", + "object-assign": "^4.1.0", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "react-redux": "^4.4.5", + "redux": "^3.6.0", + "style-loader": "^0.13.1", + "ts-loader": "^0.9.3", + "url-loader": "^0.5.7" + } +} diff --git a/17 Observables/readme.md b/17 Observables/readme.md new file mode 100644 index 0000000..4dee5ab --- /dev/null +++ b/17 Observables/readme.md @@ -0,0 +1,408 @@ +# 16 Observables + +This sample takes as starting point _04 Refactor_ + +Let's play with async calls and middleware (redux thunk). + +In this sample we are going to display a table, the data will +be retrieve from github api. + +Summary steps: + +- Let's install the needed package and typescript definitions. +- Let's register our Middleware. +- Let's create a rest api class to access this data. +- Let's define two new actions. +- Let's create an action that will trigger and async action. +- Let's add a new reducer that will hold members state. +- Let's create a memberRow component. +- Let's create a memberTable component. +- Let's create a memberArea component (include a load button). +- Let's create a memberAreaContainer. + +# Prerequisites + +Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0 or newer) if they are not already installed on your computer. + +> Verify that you are running at least node v6.x.x and npm 3.x.x by running `node -v` and `npm -v` in a terminal/console window. Older versions may produce errors. + +## Steps to build it + +- We have to install libraries and typescript definitions to handle fetch calls: redux-observable, rx and rxjs + +```bash +npm install --save-dev redux-observable rx rxjs @reactivex/rxjs @types/rx @types/es6-shim +``` + +- Let's register a Redux Epic Middleware in _./src/store.ts_: + +```javascript +import { createStore, applyMiddleware, compose } from "redux"; +import { createEpicMiddleware } from "redux-observable"; +import { rootEpic } from "./epics"; +import { reducers } from "./reducers/"; + +const epicMiddleware = createEpicMiddleware(rootEpic); + +export const store = createStore( + reducers, + compose( + applyMiddleware(epicMiddleware), + ), +); + +``` + +- And use the store in _./src/main.tsx_ + +```jsx +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import { Provider } from "react-redux"; +import { App } from "./app"; +import { store } from "./store"; + +ReactDOM.render( + + + , + document.getElementById("root") +); + +``` + +- Let's create an entity under _./src/model/member.ts_ + +```javascript +export class MemberEntity { + id: number; + login: string; + avatar_url: string; + + constructor() { + this.id = -1; + this.login = ""; + this.avatar_url = ""; + } +} +``` + +- Let's create an epic to access this data, under + +_./src/epics/fetchMembersEpic.ts_ + +```javascript +import 'rxjs'; + +// merge all actions in only one action +import { } from "rxjs/add/operator/mergeMap"; + +// map to throw a new action +import { } from "rxjs/add/operator/map"; + +import { actionsEnums } from "../common/actionsEnums"; +import { memberRequestCompleted } from "../actions/memberRequest"; +import { memberAPI } from "../restApi/memberApi"; + +// the dollar symbol in the action$ param is just a convention +export const fetchMembersEpic = action$ => + // action param is not necesary, but it will be useful + // to better understand the code + action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => + memberAPI.getAllMembers() + // memberRequestCompleted will be only an action ({type: '...', ...}) + // without "black magic" for promises + .map(memberRequestCompleted) + ); + +``` + +- Let's create a rest api class to access this data with rxjs-observable-ajax, under _./src/restApi/memberApi_ + +```javascript +import { ajax } from "rxjs/observable/dom/ajax"; + +// Sync mock data API, inspired from: +// https://gist.github.com/coryhouse/fd6232f95f9d601158e4 +class MemberAPI { + getAllMembers() { + return ( + ajax.getJSON("https://api.github.com/orgs/lemoncode/members") + ); + } +} + +export const memberAPI = new MemberAPI(); +``` + +- Update _./src/actions/memberRequest.ts_: + +```javascript +import { actionsEnums } from "../common/actionsEnums"; + +export const memberRequest = () => { + return { + type: actionsEnums.MEMBER_REQUEST_STARTED, + }; +}; +``` + +- It's time to define two new actions _./src/common/actionsEnums.ts_ + +```javascript +export const actionsEnums = { + UPDATE_USERPROFILE_NAME: "UPDATE_USERPROFILE_NAME", + UPDATE_USERPROFILE_FAVOURITE_COLOR: "UPDATE_USERPROFILE_FAVOURITE_COLOR", + MEMBER_REQUEST_STARTED: "MEMBER_REQUEST_STARTED", + MEMBER_REQUEST_COMPLETED: "MEMBER_REQUEST_COMPLETED", +}; + +``` + +- Let's create an *simple* action that will inform members once completed in _./src/actions/membersRequestCompleted.ts: + +```javascript +import { actionsEnums } from "../common/actionsEnums"; +import { MemberEntity } from "../model/member"; + +export const memberRequestCompleted = (members: MemberEntity[]) => { + // without "black magic" promises! MUAHAHAHAH! + return { + type: actionsEnums.MEMBER_REQUEST_COMPLETED, + members: members, + }; +}; + +``` + +- And _./src/actions/index.ts_ to use easily: + +```javascript +import { memberRequestCompleted } from "./memberRequestCompleted"; +import { memberRequest } from "./memberRequest"; + +export { memberRequest, memberRequestCompleted }; +``` + +- Let's add a new reducer that will hold members state + +_./src/reducers/memberReducer.ts_. + +```javascript +import {actionsEnums} from '../common/actionsEnums'; +import {MemberEntity} from '../model/member'; +import objectAssign = require('object-assign'); + +class memberState { + members : MemberEntity[]; + + public constructor() + { + this.members = []; + } +} + +export const memberReducer = (state : memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + } + + return state; +}; + + +const handleMemberRequestCompletedAction = (state : memberState, action) => { + const newState = objectAssign({}, state, {members: action.members}); + return newState; +} +``` + +- Let's register it _./src/reducers/index.ts_ + +```javascript +import { combineReducers } from 'redux'; +import { userProfileReducer } from './userProfile'; +import { memberReducer } from './memberReducer'; + + +export const reducers = combineReducers({ + userProfileReducer, + memberReducer, +}); +``` + +- Let's create a memberRow component _./src/components/members/memberrow.tsx_. + +```javascript +import * as React from 'react'; +import {MemberEntity} from '../../model/member'; + + +interface Props { + member : MemberEntity; +} + +export const MemberRow = (props: Props) => { + return ( + + + + + + {props.member.id} + + + {props.member.login} + + + ); +} +``` + +- Let's create a memberTable component under _./src/components/members/membertable.tsx_. + +```javascript +import * as React from 'react'; +import {MemberEntity} from '../../model/member'; +import {MemberRow} from './memberRow'; + +interface Props { + members: MemberEntity[]; +} + +export const MembersTable = (props: Props) => { + return ( +
+

Members Page

+ + + + + + + + + + { + props.members.map((member) => + + ) + } + +
+ Avatar + + Id + + Name +
+
+ ); +} +``` + +- Let's create a memberArea component (include a load button). + +```javascript +import * as React from 'react'; +import {MembersTable} from './memberstable'; +import {MemberEntity} from '../../model/member' + +interface Props { + loadMembers: () => any; + members: Array; +} + +export class MembersArea extends React.Component { + constructor(props: Props){ + super(props); + + this.state = {members:[]}; + + } + + render(){ + return ( +
+ +
+ this.props.loadMembers()} + /> +
+ ); + } + +} +``` + +- Let's create a memberAreaContainer. + +```javascript +import { connect } from 'react-redux'; +import { memberRequest } from '../../actions/memberRequest'; +import { MembersArea } from './memberArea'; + + +const mapStateToProps = (state) => { + return{ + members: state.memberReducer.members + }; +} + +const mapDispatchToProps = (dispatch) => { + return { + loadMembers: () => {return dispatch(memberRequest())} + }; +} + +export const MembersAreaContainer = connect( + mapStateToProps, + mapDispatchToProps +)(MembersArea) +``` + +- Let's create an _./src/components/members/index.ts_ + +```javascript +import {MembersAreaContainer} from './memberAreaContainer'; + +export { + MembersAreaContainer +} +``` + +- Let's instantiate it on _app.tsx_ + +```javascript +import * as React from 'react'; +import {HelloWorldContainer} from './components/helloworld' +import {NameEditContainer} from './components/nameEdit'; +import {ColorDisplayerContainer, ColorPickerContainer} from './components/color'; +import {MembersAreaContainer} from './components/members'; + +export const App = () => { + return ( +
+ +
+ +
+ +
+ +
+ +
+ ); +} +``` + +- Let's give a try. + +```javascript +npm start +``` diff --git a/17 Observables/src/actions/index.ts b/17 Observables/src/actions/index.ts new file mode 100644 index 0000000..13c8ce8 --- /dev/null +++ b/17 Observables/src/actions/index.ts @@ -0,0 +1,4 @@ +import { memberRequestCompleted } from "./memberRequestCompleted"; +import { memberRequest } from "./memberRequest"; + +export { memberRequest, memberRequestCompleted }; diff --git a/17 Observables/src/actions/memberRequest.ts b/17 Observables/src/actions/memberRequest.ts new file mode 100644 index 0000000..bd068b7 --- /dev/null +++ b/17 Observables/src/actions/memberRequest.ts @@ -0,0 +1,7 @@ +import { actionsEnums } from "../common/actionsEnums"; + +export const memberRequest = () => { + return { + type: actionsEnums.MEMBER_REQUEST_STARTED, + }; +}; diff --git a/17 Observables/src/actions/memberRequestCompleted.ts b/17 Observables/src/actions/memberRequestCompleted.ts new file mode 100644 index 0000000..0e7d96a --- /dev/null +++ b/17 Observables/src/actions/memberRequestCompleted.ts @@ -0,0 +1,10 @@ +import { actionsEnums } from "../common/actionsEnums"; +import { MemberEntity } from "../model/member"; + +export const memberRequestCompleted = (members: MemberEntity[]) => { + // without "black magic" promises! MUAHAHAHAH! + return { + type: actionsEnums.MEMBER_REQUEST_COMPLETED, + members: members, + }; +}; diff --git a/17 Observables/src/actions/updateFavouriteColor.ts b/17 Observables/src/actions/updateFavouriteColor.ts new file mode 100644 index 0000000..1d1de12 --- /dev/null +++ b/17 Observables/src/actions/updateFavouriteColor.ts @@ -0,0 +1,9 @@ +import { actionsEnums } from "../common/actionsEnums"; +import { Color } from "../model/color"; + +export const updateFavouriteColor = (newColor: Color) => { + return { + type: actionsEnums.UPDATE_USERPROFILE_FAVOURITE_COLOR, + newColor: newColor, + }; +}; diff --git a/17 Observables/src/actions/updateUserProfileName.ts b/17 Observables/src/actions/updateUserProfileName.ts new file mode 100644 index 0000000..655ce30 --- /dev/null +++ b/17 Observables/src/actions/updateUserProfileName.ts @@ -0,0 +1,8 @@ +import {actionsEnums} from "../common/actionsEnums"; + +export const updateUserProfileName = (newName: string) => { + return { + type: actionsEnums.UPDATE_USERPROFILE_NAME, + newName: newName + }; +}; diff --git a/17 Observables/src/app.tsx b/17 Observables/src/app.tsx new file mode 100644 index 0000000..782dc62 --- /dev/null +++ b/17 Observables/src/app.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { HelloWorldContainer } from "./components/helloworld"; +import { NameEditContainer } from "./components/nameEdit"; +import { ColorDisplayerContainer } from "./components/color"; +import { ColorPickerContainer } from "./components/color"; +import {MembersAreaContainer} from "./components/members"; + +export const App = () => { + return ( +
+ +
+ +
+ +
+ +
+ +
+ ); +}; diff --git a/17 Observables/src/common/actionsEnums.ts b/17 Observables/src/common/actionsEnums.ts new file mode 100644 index 0000000..6b91b69 --- /dev/null +++ b/17 Observables/src/common/actionsEnums.ts @@ -0,0 +1,6 @@ +export const actionsEnums = { + UPDATE_USERPROFILE_NAME: "UPDATE_USERPROFILE_NAME", + UPDATE_USERPROFILE_FAVOURITE_COLOR: "UPDATE_USERPROFILE_FAVOURITE_COLOR", + MEMBER_REQUEST_STARTED: "MEMBER_REQUEST_STARTED", + MEMBER_REQUEST_COMPLETED: "MEMBER_REQUEST_COMPLETED", +}; diff --git a/17 Observables/src/components/color/colordisplayer.tsx b/17 Observables/src/components/color/colordisplayer.tsx new file mode 100644 index 0000000..f10b6aa --- /dev/null +++ b/17 Observables/src/components/color/colordisplayer.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { Color } from "../../model/color"; + +interface Props { + color: Color; +} + +export const ColorDisplayer = (props: Props) => { + // `rgb(${props.color.red},${props.color.green}, ${props.color.blue}) })` + // "rgb(" + props.color.red + ", 40, 80)" + let divStyle = { + width: "120px", + height: "80px", + backgroundColor: `rgb(${props.color.red},${props.color.green}, ${props.color.blue})` + }; + + return ( +
+
+ ); +}; diff --git a/17 Observables/src/components/color/colordisplayerContainer.ts b/17 Observables/src/components/color/colordisplayerContainer.ts new file mode 100644 index 0000000..52b401c --- /dev/null +++ b/17 Observables/src/components/color/colordisplayerContainer.ts @@ -0,0 +1,18 @@ +import { connect } from "react-redux"; +import { ColorDisplayer } from "./colordisplayer"; + +const mapStateToProps = (state) => { + return { + color: state.userProfileReducer.favouriteColor + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + }; +}; + +export const ColorDisplayerContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ColorDisplayer); diff --git a/17 Observables/src/components/color/colorpicker.tsx b/17 Observables/src/components/color/colorpicker.tsx new file mode 100644 index 0000000..637d32d --- /dev/null +++ b/17 Observables/src/components/color/colorpicker.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { Color } from "../../model/color"; +import { ColorSlider } from "./colorslider"; + +interface Props { + color: Color; + onColorUpdated: (color: Color) => void; +} + +export const ColorPicker = (props: Props) => { + return ( +
+ props.onColorUpdated( + {red: value, green: props.color.green, blue: props.color.blue}) } + /> +
+ props.onColorUpdated( + {red: props.color.red, green: value, blue: props.color.blue}) } + /> +
+ props.onColorUpdated( + {red: props.color.red, green: props.color.green, blue: value}) } + /> +
+ ); +}; diff --git a/17 Observables/src/components/color/colorpickerContainer.ts b/17 Observables/src/components/color/colorpickerContainer.ts new file mode 100644 index 0000000..0e2867a --- /dev/null +++ b/17 Observables/src/components/color/colorpickerContainer.ts @@ -0,0 +1,23 @@ +import { connect } from "react-redux"; +import { Color } from "../../model/color"; +import { ColorPicker } from "./colorpicker"; +import { updateFavouriteColor } from "../../actions/updateFavouriteColor"; + +const mapStateToProps = (state) => { + return { + color: state.userProfileReducer.favouriteColor + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + onColorUpdated: (color: Color) => { + return dispatch(updateFavouriteColor(color)); + } + }; +}; + +export const ColorPickerContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ColorPicker); diff --git a/17 Observables/src/components/color/colorslider.tsx b/17 Observables/src/components/color/colorslider.tsx new file mode 100644 index 0000000..a0aea65 --- /dev/null +++ b/17 Observables/src/components/color/colorslider.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { Color } from "../../model/color"; + +interface Props { + value: number; + onValueUpdated: (newValue: number) => void; +} + +export const ColorSlider = (props: Props) => { + return ( +
+ props.onValueUpdated(event.target.value)} + /> + {props.value} +
+ ); +}; diff --git a/17 Observables/src/components/color/index.ts b/17 Observables/src/components/color/index.ts new file mode 100644 index 0000000..a960836 --- /dev/null +++ b/17 Observables/src/components/color/index.ts @@ -0,0 +1,7 @@ +import { ColorPickerContainer } from "./colorpickerContainer"; +import { ColorDisplayerContainer } from "./colordisplayerContainer"; + +export { + ColorPickerContainer, + ColorDisplayerContainer +} diff --git a/17 Observables/src/components/helloworld/helloWorld.tsx b/17 Observables/src/components/helloworld/helloWorld.tsx new file mode 100644 index 0000000..f443f9e --- /dev/null +++ b/17 Observables/src/components/helloworld/helloWorld.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +export const HelloWorldComponent = (props: {userName: string}) => { + return ( +

Hello Mr. {props.userName} !

+ ); +}; diff --git a/17 Observables/src/components/helloworld/helloWorldContainer.ts b/17 Observables/src/components/helloworld/helloWorldContainer.ts new file mode 100644 index 0000000..bca5ebb --- /dev/null +++ b/17 Observables/src/components/helloworld/helloWorldContainer.ts @@ -0,0 +1,18 @@ +import { connect } from "react-redux"; +import { HelloWorldComponent } from "./helloWorld"; + +const mapStateToProps = (state) => { + return { + userName: state.userProfileReducer.firstname + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + }; +}; + +export const HelloWorldContainer = connect( + mapStateToProps, + mapDispatchToProps +)(HelloWorldComponent); diff --git a/17 Observables/src/components/helloworld/index.ts b/17 Observables/src/components/helloworld/index.ts new file mode 100644 index 0000000..ec408aa --- /dev/null +++ b/17 Observables/src/components/helloworld/index.ts @@ -0,0 +1,5 @@ +import { HelloWorldContainer } from "./helloWorldContainer"; + +export { + HelloWorldContainer +} diff --git a/17 Observables/src/components/members/index.ts b/17 Observables/src/components/members/index.ts new file mode 100644 index 0000000..92631e5 --- /dev/null +++ b/17 Observables/src/components/members/index.ts @@ -0,0 +1,5 @@ +import {MembersAreaContainer} from './memberAreaContainer'; + +export { + MembersAreaContainer +} diff --git a/17 Observables/src/components/members/memberAreaContainer.ts b/17 Observables/src/components/members/memberAreaContainer.ts new file mode 100644 index 0000000..609c804 --- /dev/null +++ b/17 Observables/src/components/members/memberAreaContainer.ts @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { memberRequest } from '../../actions/memberRequest'; +import { MembersArea } from './memberarea'; + + +const mapStateToProps = (state) => { + return{ + members: state.memberReducer.members + }; +} + +const mapDispatchToProps = (dispatch) => { + return { + loadMembers: () => {return dispatch(memberRequest())} + }; +} + +export const MembersAreaContainer = connect( + mapStateToProps, + mapDispatchToProps +)(MembersArea) diff --git a/17 Observables/src/components/members/memberarea.tsx b/17 Observables/src/components/members/memberarea.tsx new file mode 100644 index 0000000..4f1d435 --- /dev/null +++ b/17 Observables/src/components/members/memberarea.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import {MembersTable} from './membertable'; +import {MemberEntity} from '../../model/member' + +interface Props { + loadMembers: () => any; + members: Array; +} + +export class MembersArea extends React.Component { + constructor(props: Props){ + super(props); + + this.state = {members:[]}; + + } + + render(){ + return ( +
+ +
+ this.props.loadMembers()} + /> +
+ ); + } + +} diff --git a/17 Observables/src/components/members/memberrow.tsx b/17 Observables/src/components/members/memberrow.tsx new file mode 100644 index 0000000..3e85251 --- /dev/null +++ b/17 Observables/src/components/members/memberrow.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import {MemberEntity} from '../../model/member'; + + +interface Props { + member : MemberEntity; +} + +export const MemberRow = (props: Props) => { + return ( + + + + + + {props.member.id} + + + {props.member.login} + + + ); +} diff --git a/17 Observables/src/components/members/membertable.tsx b/17 Observables/src/components/members/membertable.tsx new file mode 100644 index 0000000..5c7a408 --- /dev/null +++ b/17 Observables/src/components/members/membertable.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import {MemberEntity} from '../../model/member'; +import {MemberRow} from './memberrow'; + +interface Props { + members: MemberEntity[]; +} + +export const MembersTable = (props: Props) => { + return ( +
+

Members Page

+ + + + + + + + + + { + props.members.map((member) => + + ) + } + +
+ Avatar + + Id + + Name +
+
+ ); +} diff --git a/17 Observables/src/components/nameEdit/index.ts b/17 Observables/src/components/nameEdit/index.ts new file mode 100644 index 0000000..096f772 --- /dev/null +++ b/17 Observables/src/components/nameEdit/index.ts @@ -0,0 +1,5 @@ +import { NameEditContainer } from "./nameEditContainer"; + +export { + NameEditContainer +} diff --git a/17 Observables/src/components/nameEdit/nameEdit.tsx b/17 Observables/src/components/nameEdit/nameEdit.tsx new file mode 100644 index 0000000..ce45916 --- /dev/null +++ b/17 Observables/src/components/nameEdit/nameEdit.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +export const NameEditComponent = (props: {userName: string, onChange: (name: string) => any}) => { + return ( +
+ + props.onChange(e.target.value)} + /> +
+ ); +}; diff --git a/17 Observables/src/components/nameEdit/nameEditContainer.ts b/17 Observables/src/components/nameEdit/nameEditContainer.ts new file mode 100644 index 0000000..cfd1ab1 --- /dev/null +++ b/17 Observables/src/components/nameEdit/nameEditContainer.ts @@ -0,0 +1,22 @@ +import { connect } from "react-redux"; +import { NameEditComponent } from "./nameEdit"; +import { updateUserProfileName } from "../../actions/updateUserProfileName"; + +const mapStateToProps = (state) => { + return { + userName: state.userProfileReducer.firstname + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + onChange: (name: string) => { + return dispatch(updateUserProfileName(name)); + } + }; +}; + +export const NameEditContainer = connect( + mapStateToProps, + mapDispatchToProps +)(NameEditComponent); diff --git a/17 Observables/src/epics/fetchMembersEpic.ts b/17 Observables/src/epics/fetchMembersEpic.ts new file mode 100644 index 0000000..e85348a --- /dev/null +++ b/17 Observables/src/epics/fetchMembersEpic.ts @@ -0,0 +1,22 @@ +import 'rxjs'; + +// merge all actions in only one action +import { } from "rxjs/add/operator/mergeMap"; + +// map to throw a new action +import { } from "rxjs/add/operator/map"; + +import { actionsEnums } from "../common/actionsEnums"; +import { memberRequest, memberRequestCompleted } from "../actions/"; +import { memberAPI } from "../restApi/memberApi"; + +// the dollar symbol in the action$ param is just a convention +export const fetchMembersEpic = action$ => + // action param is not necesary, but it will be useful + // to better understand the code + action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => + memberAPI.getAllMembers() + // memberRequestCompleted will be only an action ({type: '...', ...}) + // without "black magic" for promises + .map(memberRequestCompleted) + ); diff --git a/17 Observables/src/epics/index.ts b/17 Observables/src/epics/index.ts new file mode 100644 index 0000000..6288c0a --- /dev/null +++ b/17 Observables/src/epics/index.ts @@ -0,0 +1,5 @@ +import { combineEpics } from "redux-observable"; + +import { fetchMembersEpic } from "./fetchMembersEpic"; + +export const rootEpic = combineEpics(fetchMembersEpic); diff --git a/17 Observables/src/index.html b/17 Observables/src/index.html new file mode 100644 index 0000000..4b32a83 --- /dev/null +++ b/17 Observables/src/index.html @@ -0,0 +1,12 @@ + + + + + + + +

Sample app

+
+
+ + diff --git a/17 Observables/src/main.tsx b/17 Observables/src/main.tsx new file mode 100644 index 0000000..65d5e6b --- /dev/null +++ b/17 Observables/src/main.tsx @@ -0,0 +1,12 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import { Provider } from "react-redux"; +import { App } from "./app"; +import { store } from "./store"; + +ReactDOM.render( + + + , + document.getElementById("root") +); diff --git a/17 Observables/src/model/color.ts b/17 Observables/src/model/color.ts new file mode 100644 index 0000000..e1a342b --- /dev/null +++ b/17 Observables/src/model/color.ts @@ -0,0 +1,5 @@ +export class Color { + red: number; + green: number; + blue: number; +} diff --git a/17 Observables/src/model/member.ts b/17 Observables/src/model/member.ts new file mode 100644 index 0000000..d5649c3 --- /dev/null +++ b/17 Observables/src/model/member.ts @@ -0,0 +1,11 @@ +export class MemberEntity { + id: number; + login: string; + avatar_url: string; + + constructor() { + this.id = -1; + this.login = ""; + this.avatar_url = ""; + } +} diff --git a/17 Observables/src/reducers/index.ts b/17 Observables/src/reducers/index.ts new file mode 100644 index 0000000..43ae873 --- /dev/null +++ b/17 Observables/src/reducers/index.ts @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import { userProfileReducer } from "./userProfile"; +import { memberReducer } from "./memberReducer"; + +export const reducers = combineReducers({ + userProfileReducer, + memberReducer, +}); diff --git a/17 Observables/src/reducers/memberReducer.ts b/17 Observables/src/reducers/memberReducer.ts new file mode 100644 index 0000000..8cf5614 --- /dev/null +++ b/17 Observables/src/reducers/memberReducer.ts @@ -0,0 +1,27 @@ +import {actionsEnums} from '../common/actionsEnums'; +import {MemberEntity} from '../model/member'; +import objectAssign = require('object-assign'); + +class memberState { + members : MemberEntity[]; + + public constructor() + { + this.members = []; + } +} + +export const memberReducer = (state : memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + } + + return state; +}; + + +const handleMemberRequestCompletedAction = (state : memberState, action) => { + const newState = objectAssign({}, state, {members: action.members}); + return newState; +} diff --git a/17 Observables/src/reducers/userProfile.ts b/17 Observables/src/reducers/userProfile.ts new file mode 100644 index 0000000..858c825 --- /dev/null +++ b/17 Observables/src/reducers/userProfile.ts @@ -0,0 +1,35 @@ +import { actionsEnums } from "../common/actionsEnums"; +import { updateUserProfileName } from "../actions/updateUserProfileName"; +import { Color } from "../model/color"; +import objectAssign = require("object-assign"); + +class UserProfileState { + firstname: string; + favouriteColor: Color; + + public constructor() { + this.firstname = "Default name"; + this.favouriteColor = {red: 0, green: 0, blue: 180}; + } +} + +export const userProfileReducer = (state: UserProfileState = new UserProfileState(), action) => { + switch (action.type) { + case actionsEnums.UPDATE_USERPROFILE_NAME: + return handleUserProfileAction(state, action); + case actionsEnums.UPDATE_USERPROFILE_FAVOURITE_COLOR: + return handleFavouriteColorAction(state, action); + } + + return state; +}; + +const handleFavouriteColorAction = (state: UserProfileState, action) => { + const newState = objectAssign({}, state, {favouriteColor: action.newColor}); + return newState; +}; + +const handleUserProfileAction = (state: UserProfileState, action) => { + const newState = objectAssign({}, state, {firstname: action.newName}); + return newState; +}; diff --git a/17 Observables/src/restApi/memberApi.ts b/17 Observables/src/restApi/memberApi.ts new file mode 100644 index 0000000..b1aabdc --- /dev/null +++ b/17 Observables/src/restApi/memberApi.ts @@ -0,0 +1,13 @@ +import { ajax } from "rxjs/observable/dom/ajax"; + +// Sync mock data API, inspired from: +// https://gist.github.com/coryhouse/fd6232f95f9d601158e4 +class MemberAPI { + getAllMembers() { + return ( + ajax.getJSON("https://api.github.com/orgs/lemoncode/members") + ); + } +} + +export const memberAPI = new MemberAPI(); diff --git a/17 Observables/src/store.ts b/17 Observables/src/store.ts new file mode 100644 index 0000000..faf7326 --- /dev/null +++ b/17 Observables/src/store.ts @@ -0,0 +1,13 @@ +import { createStore, applyMiddleware, compose } from "redux"; +import { createEpicMiddleware } from "redux-observable"; +import { rootEpic } from "./epics"; +import { reducers } from "./reducers/"; + +const epicMiddleware = createEpicMiddleware(rootEpic); + +export const store = createStore( + reducers, + compose( + applyMiddleware(epicMiddleware), + ), +); diff --git a/17 Observables/tsconfig.json b/17 Observables/tsconfig.json new file mode 100644 index 0000000..3cd7705 --- /dev/null +++ b/17 Observables/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "jsx": "react", + "sourceMap": true, + "noLib": false, + "suppressImplicitAnyIndexErrors": true + }, + "compileOnSave": false, + "exclude": [ + "node_modules" + ] +} diff --git a/17 Observables/webpack.config.js b/17 Observables/webpack.config.js new file mode 100644 index 0000000..e16553d --- /dev/null +++ b/17 Observables/webpack.config.js @@ -0,0 +1,71 @@ +var path = require('path'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); + +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: ['', '.js', '.ts', '.tsx'] + }, + + entry: [ + './main.tsx', + '../node_modules/bootstrap/dist/css/bootstrap.css' + ], + output: { + path: path.join(basePath, 'dist'), + filename: 'bundle.js' + }, + + devtool: 'source-map', + + devServer: { + contentBase: './dist', //Content base + inline: true, //Enable watch and live reload + host: 'localhost', + port: 8080, + stats: 'errors-only' + }, + + module: { + loaders: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: 'ts-loader' + }, + { + test: /\.css$/, + loader: 'style-loader!css-loader' + }, + // Loading glyphicons => https://github.com/gowravshekar/bootstrap-webpack + // Using here url-loader and file-loader + { + test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=application/font-woff' + }, + { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=application/octet-stream' + }, + { + test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, + loader: 'file' + }, + { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=image/svg+xml' + } + ] + }, + plugins: [ + // Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', // Name of file in ./dist/ + template: 'index.html', // Name of template in ./src + hash: true + }) + ] +} From 0c7584723b76439a94929eabf60ea27c1b11ce5f Mon Sep 17 00:00:00 2001 From: linuxonrails Date: Thu, 15 Dec 2016 11:57:20 +0100 Subject: [PATCH 2/5] Small fixes in 17 Observables --- 17 Observables/package.json | 3 +- 17 Observables/readme.md | 229 ++++++++++-------- 17 Observables/src/app.tsx | 13 +- .../src/components/members/index.ts | 2 +- .../src/components/members/memberArea.tsx | 31 +++ .../components/members/memberAreaContainer.ts | 19 +- .../src/components/members/memberRow.tsx | 22 ++ .../src/components/members/memberTable.tsx | 37 +++ .../src/components/members/memberarea.tsx | 32 --- .../src/components/members/memberrow.tsx | 23 -- .../src/components/members/membertable.tsx | 37 --- 17 Observables/src/epics/fetchMembersEpic.ts | 2 +- 17 Observables/src/reducers/index.ts | 6 +- 17 Observables/src/reducers/memberReducer.ts | 25 +- 14 files changed, 248 insertions(+), 233 deletions(-) create mode 100644 17 Observables/src/components/members/memberArea.tsx create mode 100644 17 Observables/src/components/members/memberRow.tsx create mode 100644 17 Observables/src/components/members/memberTable.tsx delete mode 100644 17 Observables/src/components/members/memberarea.tsx delete mode 100644 17 Observables/src/components/members/memberrow.tsx delete mode 100644 17 Observables/src/components/members/membertable.tsx diff --git a/17 Observables/package.json b/17 Observables/package.json index 8ea9780..9f5b8d6 100644 --- a/17 Observables/package.json +++ b/17 Observables/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "devDependencies": { - "@reactivex/rxjs": "^5.0.1", + "@types/es6-shim": "^0.31.32", "@types/rx": "^2.5.34", "redux-observable": "^0.12.2", "rx": "^4.1.0", @@ -21,7 +21,6 @@ "webpack-dev-server": "^1.16.2" }, "dependencies": { - "@types/es6-shim": "^0.31.32", "@types/object-assign": "^4.0.30", "@types/react": "^0.14.43", "@types/react-dom": "^0.14.18", diff --git a/17 Observables/readme.md b/17 Observables/readme.md index 4dee5ab..affa755 100644 --- a/17 Observables/readme.md +++ b/17 Observables/readme.md @@ -2,7 +2,7 @@ This sample takes as starting point _04 Refactor_ -Let's play with async calls and middleware (redux thunk). +Let's play with async calls and epic middleware (redux observables). In this sample we are going to display a table, the data will be retrieve from github api. @@ -20,6 +20,10 @@ Summary steps: - Let's create a memberArea component (include a load button). - Let's create a memberAreaContainer. +Additional step: + +- Let's define two new actions (ajax-with-delay and cancel). + # Prerequisites Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0 or newer) if they are not already installed on your computer. @@ -28,13 +32,19 @@ Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0 or newer) if they are ## Steps to build it +- Copy the content from _04 Refactor_ and execute: + + ```bash + npm install + ``` + - We have to install libraries and typescript definitions to handle fetch calls: redux-observable, rx and rxjs ```bash -npm install --save-dev redux-observable rx rxjs @reactivex/rxjs @types/rx @types/es6-shim +npm install --save-dev redux-observable rx rxjs @types/rx @types/es6-shim ``` -- Let's register a Redux Epic Middleware in _./src/store.ts_: +- Let's register a Redux Epic Middleware in _./src/store.ts_ ```javascript import { createStore, applyMiddleware, compose } from "redux"; @@ -87,9 +97,7 @@ export class MemberEntity { } ``` -- Let's create an epic to access this data, under - -_./src/epics/fetchMembersEpic.ts_ +- Let's create an epic to access this data, under _./src/epics/fetchMembersEpic.ts_ ```javascript import 'rxjs'; @@ -101,7 +109,7 @@ import { } from "rxjs/add/operator/mergeMap"; import { } from "rxjs/add/operator/map"; import { actionsEnums } from "../common/actionsEnums"; -import { memberRequestCompleted } from "../actions/memberRequest"; +import { memberRequestCompleted } from "../actions/"; import { memberAPI } from "../restApi/memberApi"; // the dollar symbol in the action$ param is just a convention @@ -117,7 +125,17 @@ export const fetchMembersEpic = action$ => ``` -- Let's create a rest api class to access this data with rxjs-observable-ajax, under _./src/restApi/memberApi_ +_./src/epics/index.ts_ + +```javascript +import { combineEpics } from "redux-observable"; + +import { fetchMembersEpic } from "./fetchMembersEpic"; + +export const rootEpic = combineEpics(fetchMembersEpic); +``` + +- Let's create a rest api class to access this data with rxjs-observable-ajax, under _./src/restApi/memberApi.ts_ ```javascript import { ajax } from "rxjs/observable/dom/ajax"; @@ -135,7 +153,7 @@ class MemberAPI { export const memberAPI = new MemberAPI(); ``` -- Update _./src/actions/memberRequest.ts_: +- Create _./src/actions/memberRequest.ts_ ```javascript import { actionsEnums } from "../common/actionsEnums"; @@ -159,7 +177,7 @@ export const actionsEnums = { ``` -- Let's create an *simple* action that will inform members once completed in _./src/actions/membersRequestCompleted.ts: +- Let's create an *simple* action that will inform members once completed in _./src/actions/memberRequestCompleted.ts_ ```javascript import { actionsEnums } from "../common/actionsEnums"; @@ -186,36 +204,36 @@ export { memberRequest, memberRequestCompleted }; - Let's add a new reducer that will hold members state -_./src/reducers/memberReducer.ts_. +_./src/reducers/memberReducer.ts_ ```javascript -import {actionsEnums} from '../common/actionsEnums'; -import {MemberEntity} from '../model/member'; -import objectAssign = require('object-assign'); +import { actionsEnums } from "../common/actionsEnums"; +import { MemberEntity } from "../model/member"; +import objectAssign = require("object-assign"); class memberState { - members : MemberEntity[]; + members: MemberEntity[]; - public constructor() - { + public constructor() { this.members = []; } } -export const memberReducer = (state : memberState = new memberState(), action) => { - switch (action.type) { - case actionsEnums.MEMBER_REQUEST_COMPLETED: - return handleMemberRequestCompletedAction(state, action); - } +export const memberReducer = (state: memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + } - return state; + return state; }; -const handleMemberRequestCompletedAction = (state : memberState, action) => { +const handleMemberRequestCompletedAction = (state: memberState, action) => { const newState = objectAssign({}, state, {members: action.members}); return newState; } + ``` - Let's register it _./src/reducers/index.ts_ @@ -225,122 +243,124 @@ import { combineReducers } from 'redux'; import { userProfileReducer } from './userProfile'; import { memberReducer } from './memberReducer'; - export const reducers = combineReducers({ userProfileReducer, memberReducer, }); ``` -- Let's create a memberRow component _./src/components/members/memberrow.tsx_. - -```javascript -import * as React from 'react'; -import {MemberEntity} from '../../model/member'; +- Let's create a `memberRow` component _./src/components/members/memberRow.tsx_ +```jsx +import * as React from "react"; +import { MemberEntity } from "../../model/member"; interface Props { - member : MemberEntity; + member: MemberEntity; } export const MemberRow = (props: Props) => { - return ( - - - - - - {props.member.id} - - - {props.member.login} - - - ); + return ( + + + + + + {props.member.id} + + + {props.member.login} + + + ); } + ``` -- Let's create a memberTable component under _./src/components/members/membertable.tsx_. +- Let's create a memberTable component under _./src/components/members/memberTable.tsx_ -```javascript -import * as React from 'react'; -import {MemberEntity} from '../../model/member'; -import {MemberRow} from './memberRow'; +```jsx +import * as React from "react"; +import { MemberEntity } from "../../model/member"; +import { MemberRow } from "./memberRow"; interface Props { - members: MemberEntity[]; + members: MemberEntity[]; } export const MembersTable = (props: Props) => { - return ( -
-

Members Page

- - - - - - - - - - { - props.members.map((member) => - - ) - } - -
- Avatar - - Id - - Name -
-
- ); + return ( +
+

Members Page

+ + + + + + + + + + { + props.members.map((member) => + + ) + } + +
+ Avatar + + Id + + Name +
+
+ ); } + ``` -- Let's create a memberArea component (include a load button). +- Let's create a memberArea component (include a load button) in _./src/components/members/memberArea.tsx_ -```javascript +```jsx import * as React from 'react'; -import {MembersTable} from './memberstable'; -import {MemberEntity} from '../../model/member' +import { MembersTable } from './memberTable'; +import { MemberEntity } from '../../model/member' interface Props { - loadMembers: () => any; - members: Array; + loadMembers: () => any; + members: Array; } export class MembersArea extends React.Component { - constructor(props: Props){ - super(props); - - this.state = {members:[]}; - - } - - render(){ - return ( -
- -
- this.props.loadMembers()} - /> -
- ); - } + constructor(props: Props){ + super(props); + this.state = {members:[]}; + } + + render(){ + return ( +
+ +
+ this.props.loadMembers()} + /> +
+ ); + } } + ``` - Let's create a memberAreaContainer. +_./src/components/members/memberAreaContainer.ts_ + ```javascript import { connect } from 'react-redux'; import { memberRequest } from '../../actions/memberRequest'; @@ -368,11 +388,12 @@ export const MembersAreaContainer = connect( - Let's create an _./src/components/members/index.ts_ ```javascript -import {MembersAreaContainer} from './memberAreaContainer'; +import { MembersAreaContainer } from './memberAreaContainer'; export { MembersAreaContainer } + ``` - Let's instantiate it on _app.tsx_ diff --git a/17 Observables/src/app.tsx b/17 Observables/src/app.tsx index 782dc62..bfdd5d0 100644 --- a/17 Observables/src/app.tsx +++ b/17 Observables/src/app.tsx @@ -1,9 +1,8 @@ -import * as React from "react"; -import { HelloWorldContainer } from "./components/helloworld"; -import { NameEditContainer } from "./components/nameEdit"; -import { ColorDisplayerContainer } from "./components/color"; -import { ColorPickerContainer } from "./components/color"; -import {MembersAreaContainer} from "./components/members"; +import * as React from 'react'; +import { HelloWorldContainer } from './components/helloworld' +import { NameEditContainer } from './components/nameEdit'; +import { ColorDisplayerContainer, ColorPickerContainer } from './components/color'; +import { MembersAreaContainer } from './components/members'; export const App = () => { return ( @@ -19,4 +18,4 @@ export const App = () => { ); -}; +} diff --git a/17 Observables/src/components/members/index.ts b/17 Observables/src/components/members/index.ts index 92631e5..13cb8e3 100644 --- a/17 Observables/src/components/members/index.ts +++ b/17 Observables/src/components/members/index.ts @@ -1,4 +1,4 @@ -import {MembersAreaContainer} from './memberAreaContainer'; +import { MembersAreaContainer } from './memberAreaContainer'; export { MembersAreaContainer diff --git a/17 Observables/src/components/members/memberArea.tsx b/17 Observables/src/components/members/memberArea.tsx new file mode 100644 index 0000000..583a952 --- /dev/null +++ b/17 Observables/src/components/members/memberArea.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { MembersTable } from './memberTable'; +import { MemberEntity } from '../../model/member' + +interface Props { + loadMembers: () => any; + members: Array; +} + +export class MembersArea extends React.Component { + constructor(props: Props){ + super(props); + + this.state = {members:[]}; + } + + render(){ + return ( +
+ +
+ this.props.loadMembers()} + /> +
+ ); + } +} diff --git a/17 Observables/src/components/members/memberAreaContainer.ts b/17 Observables/src/components/members/memberAreaContainer.ts index 609c804..eed9b28 100644 --- a/17 Observables/src/components/members/memberAreaContainer.ts +++ b/17 Observables/src/components/members/memberAreaContainer.ts @@ -1,21 +1,20 @@ import { connect } from 'react-redux'; import { memberRequest } from '../../actions/memberRequest'; -import { MembersArea } from './memberarea'; - +import { MembersArea } from './memberArea'; const mapStateToProps = (state) => { - return{ - members: state.memberReducer.members - }; + return{ + members: state.memberReducer.members + }; } const mapDispatchToProps = (dispatch) => { - return { - loadMembers: () => {return dispatch(memberRequest())} - }; + return { + loadMembers: () => {return dispatch(memberRequest())} + }; } export const MembersAreaContainer = connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps, + mapDispatchToProps )(MembersArea) diff --git a/17 Observables/src/components/members/memberRow.tsx b/17 Observables/src/components/members/memberRow.tsx new file mode 100644 index 0000000..6db5a79 --- /dev/null +++ b/17 Observables/src/components/members/memberRow.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { MemberEntity } from "../../model/member"; + +interface Props { + member: MemberEntity; +} + +export const MemberRow = (props: Props) => { + return ( + + + + + + {props.member.id} + + + {props.member.login} + + + ); +} diff --git a/17 Observables/src/components/members/memberTable.tsx b/17 Observables/src/components/members/memberTable.tsx new file mode 100644 index 0000000..2141b4b --- /dev/null +++ b/17 Observables/src/components/members/memberTable.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import { MemberEntity } from "../../model/member"; +import { MemberRow } from "./memberRow"; + +interface Props { + members: MemberEntity[]; +} + +export const MembersTable = (props: Props) => { + return ( +
+

Members Page

+ + + + + + + + + + { + props.members.map((member) => + + ) + } + +
+ Avatar + + Id + + Name +
+
+ ); +} diff --git a/17 Observables/src/components/members/memberarea.tsx b/17 Observables/src/components/members/memberarea.tsx deleted file mode 100644 index 4f1d435..0000000 --- a/17 Observables/src/components/members/memberarea.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import {MembersTable} from './membertable'; -import {MemberEntity} from '../../model/member' - -interface Props { - loadMembers: () => any; - members: Array; -} - -export class MembersArea extends React.Component { - constructor(props: Props){ - super(props); - - this.state = {members:[]}; - - } - - render(){ - return ( -
- -
- this.props.loadMembers()} - /> -
- ); - } - -} diff --git a/17 Observables/src/components/members/memberrow.tsx b/17 Observables/src/components/members/memberrow.tsx deleted file mode 100644 index 3e85251..0000000 --- a/17 Observables/src/components/members/memberrow.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; -import {MemberEntity} from '../../model/member'; - - -interface Props { - member : MemberEntity; -} - -export const MemberRow = (props: Props) => { - return ( - - - - - - {props.member.id} - - - {props.member.login} - - - ); -} diff --git a/17 Observables/src/components/members/membertable.tsx b/17 Observables/src/components/members/membertable.tsx deleted file mode 100644 index 5c7a408..0000000 --- a/17 Observables/src/components/members/membertable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -import {MemberEntity} from '../../model/member'; -import {MemberRow} from './memberrow'; - -interface Props { - members: MemberEntity[]; -} - -export const MembersTable = (props: Props) => { - return ( -
-

Members Page

- - - - - - - - - - { - props.members.map((member) => - - ) - } - -
- Avatar - - Id - - Name -
-
- ); -} diff --git a/17 Observables/src/epics/fetchMembersEpic.ts b/17 Observables/src/epics/fetchMembersEpic.ts index e85348a..0e30713 100644 --- a/17 Observables/src/epics/fetchMembersEpic.ts +++ b/17 Observables/src/epics/fetchMembersEpic.ts @@ -7,7 +7,7 @@ import { } from "rxjs/add/operator/mergeMap"; import { } from "rxjs/add/operator/map"; import { actionsEnums } from "../common/actionsEnums"; -import { memberRequest, memberRequestCompleted } from "../actions/"; +import { memberRequestCompleted } from "../actions/"; import { memberAPI } from "../restApi/memberApi"; // the dollar symbol in the action$ param is just a convention diff --git a/17 Observables/src/reducers/index.ts b/17 Observables/src/reducers/index.ts index 43ae873..9ec1172 100644 --- a/17 Observables/src/reducers/index.ts +++ b/17 Observables/src/reducers/index.ts @@ -1,6 +1,6 @@ -import { combineReducers } from "redux"; -import { userProfileReducer } from "./userProfile"; -import { memberReducer } from "./memberReducer"; +import { combineReducers } from 'redux'; +import { userProfileReducer } from './userProfile'; +import { memberReducer } from './memberReducer'; export const reducers = combineReducers({ userProfileReducer, diff --git a/17 Observables/src/reducers/memberReducer.ts b/17 Observables/src/reducers/memberReducer.ts index 8cf5614..d2126ca 100644 --- a/17 Observables/src/reducers/memberReducer.ts +++ b/17 Observables/src/reducers/memberReducer.ts @@ -1,27 +1,26 @@ -import {actionsEnums} from '../common/actionsEnums'; -import {MemberEntity} from '../model/member'; -import objectAssign = require('object-assign'); +import { actionsEnums } from "../common/actionsEnums"; +import { MemberEntity } from "../model/member"; +import objectAssign = require("object-assign"); class memberState { - members : MemberEntity[]; + members: MemberEntity[]; - public constructor() - { + public constructor() { this.members = []; } } -export const memberReducer = (state : memberState = new memberState(), action) => { - switch (action.type) { - case actionsEnums.MEMBER_REQUEST_COMPLETED: - return handleMemberRequestCompletedAction(state, action); - } +export const memberReducer = (state: memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + } - return state; + return state; }; -const handleMemberRequestCompletedAction = (state : memberState, action) => { +const handleMemberRequestCompletedAction = (state: memberState, action) => { const newState = objectAssign({}, state, {members: action.members}); return newState; } From 0c9f5b538354c5f436b2375e04bfc75bf8202baf Mon Sep 17 00:00:00 2001 From: linuxonrails Date: Thu, 15 Dec 2016 12:09:35 +0100 Subject: [PATCH 3/5] More small fixes in 17 Observables --- 17 Observables/readme.md | 42 ++++++++++--------- 17 Observables/src/app.tsx | 11 ++--- .../components/members/memberAreaContainer.ts | 10 ++--- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/17 Observables/readme.md b/17 Observables/readme.md index affa755..fc0dd9e 100644 --- a/17 Observables/readme.md +++ b/17 Observables/readme.md @@ -362,27 +362,27 @@ export class MembersArea extends React.Component { _./src/components/members/memberAreaContainer.ts_ ```javascript -import { connect } from 'react-redux'; -import { memberRequest } from '../../actions/memberRequest'; -import { MembersArea } from './memberArea'; - +import { connect } from "react-redux"; +import { memberRequest } from "../../actions/memberRequest"; +import { MembersArea } from "./memberArea"; const mapStateToProps = (state) => { - return{ - members: state.memberReducer.members - }; + return { + members: state.memberReducer.members + }; } const mapDispatchToProps = (dispatch) => { - return { - loadMembers: () => {return dispatch(memberRequest())} - }; + return { + loadMembers: () => {return dispatch(memberRequest())} + }; } export const MembersAreaContainer = connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps, + mapDispatchToProps, )(MembersArea) + ``` - Let's create an _./src/components/members/index.ts_ @@ -398,12 +398,13 @@ export { - Let's instantiate it on _app.tsx_ -```javascript -import * as React from 'react'; -import {HelloWorldContainer} from './components/helloworld' -import {NameEditContainer} from './components/nameEdit'; -import {ColorDisplayerContainer, ColorPickerContainer} from './components/color'; -import {MembersAreaContainer} from './components/members'; +```jsx +import * as React from "react"; +import { MembersAreaContainer } from './components/members'; +import { HelloWorldContainer } from "./components/helloworld"; +import { NameEditContainer } from "./components/nameEdit"; +import { ColorDisplayerContainer } from "./components/color"; +import { ColorPickerContainer } from "./components/color"; export const App = () => { return ( @@ -419,11 +420,12 @@ export const App = () => { ); -} +}; + ``` - Let's give a try. -```javascript +```shell npm start ``` diff --git a/17 Observables/src/app.tsx b/17 Observables/src/app.tsx index bfdd5d0..b35ac19 100644 --- a/17 Observables/src/app.tsx +++ b/17 Observables/src/app.tsx @@ -1,8 +1,9 @@ -import * as React from 'react'; -import { HelloWorldContainer } from './components/helloworld' -import { NameEditContainer } from './components/nameEdit'; -import { ColorDisplayerContainer, ColorPickerContainer } from './components/color'; +import * as React from "react"; import { MembersAreaContainer } from './components/members'; +import { HelloWorldContainer } from "./components/helloworld"; +import { NameEditContainer } from "./components/nameEdit"; +import { ColorDisplayerContainer } from "./components/color"; +import { ColorPickerContainer } from "./components/color"; export const App = () => { return ( @@ -18,4 +19,4 @@ export const App = () => { ); -} +}; diff --git a/17 Observables/src/components/members/memberAreaContainer.ts b/17 Observables/src/components/members/memberAreaContainer.ts index eed9b28..ea41b4e 100644 --- a/17 Observables/src/components/members/memberAreaContainer.ts +++ b/17 Observables/src/components/members/memberAreaContainer.ts @@ -1,9 +1,9 @@ -import { connect } from 'react-redux'; -import { memberRequest } from '../../actions/memberRequest'; -import { MembersArea } from './memberArea'; +import { connect } from "react-redux"; +import { memberRequest } from "../../actions/memberRequest"; +import { MembersArea } from "./memberArea"; const mapStateToProps = (state) => { - return{ + return { members: state.memberReducer.members }; } @@ -16,5 +16,5 @@ const mapDispatchToProps = (dispatch) => { export const MembersAreaContainer = connect( mapStateToProps, - mapDispatchToProps + mapDispatchToProps, )(MembersArea) From 1532dd6c1d716ff9a08b69df97438416039946f1 Mon Sep 17 00:00:00 2001 From: linuxonrails Date: Thu, 15 Dec 2016 13:14:16 +0100 Subject: [PATCH 4/5] Additional step to cancel the ajax query in 17 Observables --- 17 Observables/readme.md | 188 ++++++++++++++++++ 17 Observables/src/actions/index.ts | 3 +- .../src/actions/memberRequestCancelled.ts | 7 + 17 Observables/src/common/actionsEnums.ts | 1 + .../src/components/members/memberArea.tsx | 33 ++- .../components/members/memberAreaContainer.ts | 8 +- 17 Observables/src/epics/fetchMembersEpic.ts | 5 + 17 Observables/src/reducers/memberReducer.ts | 25 ++- 17 Observables/src/store.ts | 1 + 9 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 17 Observables/src/actions/memberRequestCancelled.ts diff --git a/17 Observables/readme.md b/17 Observables/readme.md index fc0dd9e..395875b 100644 --- a/17 Observables/readme.md +++ b/17 Observables/readme.md @@ -429,3 +429,191 @@ export const App = () => { ```shell npm start ``` + +------------------------------------------------------------------------------------------------------------------------------------------------ + +## Additional step + +- We will add DevToolsExtensions support to see better the behavior: + +_./src/store.ts_ + +```javascript + // ... + reducers, + compose( + applyMiddleware(epicMiddleware), + window['devToolsExtension'] ? window['devToolsExtension']() : f => f + ), +); +``` + +- We will add a delay in the ajax request and cancel behavior: + +_./src/common/actionsEnums.ts_ + +```javascript +export const actionsEnums = { + // ... + MEMBER_REQUEST_CANCELLED: "MEMBER_REQUEST_CANCELLED", +}; +``` + +_./src/actions/memberRequestCancelled.ts_ + +```javascript +import { actionsEnums } from "../common/actionsEnums"; + +export const memberRequestCancelled = () => { + return { + type: actionsEnums.MEMBER_REQUEST_CANCELLED, + }; +}; +``` + +_./src/actions/index.ts_ + +```javascript +import { memberRequestCompleted } from "./memberRequestCompleted"; +import { memberRequest } from "./memberRequest"; +import { memberRequestCancelled } from "./memberRequestCancelled"; + +export { memberRequest, memberRequestCompleted, memberRequestCancelled }; +``` + +_./src/epics/fetchMembersEpic.ts_ + +```javascript +// ... + +import { } from "rxjs/add/operator/delay"; + +// ... +export const fetchMembersEpic = action$ => + // ... + action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => + memberAPI.getAllMembers() + // Asynchronously wait 2000ms then continue + .delay(2000) + // memberRequestCompleted will be only an action ({type: '...', ...}) + // without "black magic" for promises + .map(memberRequestCompleted) + .takeUntil(action$.ofType(actionsEnums.MEMBER_REQUEST_CANCELLED)) + ); + +``` + +- We add a button to cancel the `MEMBER_REQUEST_STARTED` action and a members_loading indicator in state: + +_./src/components/members/memberAreaContainer.ts_ + +```javascript +import { connect } from "react-redux"; +import { memberRequest, memberRequestCancelled } from "../../actions/"; +import { MembersArea } from "./memberArea"; + +const mapStateToProps = (state) => { + return { + members: state.memberReducer.members, + members_loading: state.memberReducer.members_loading, + }; +} + +const mapDispatchToProps = (dispatch) => { + return { + loadMembers: () => {return dispatch(memberRequest())}, + cancelLoadMembers: () => {return dispatch(memberRequestCancelled())}, + }; +} + +export const MembersAreaContainer = connect( + mapStateToProps, + mapDispatchToProps, +)(MembersArea) +``` + +```jsx +// ... + +interface Props { + loadMembers: () => any; + cancelLoadMembers: () => any; + members: Array; + members_loading: boolean; +} + +export class MembersArea extends React.Component { + // ... + + render(){ + return ( +
+ +
+
+ + { + this.props.members_loading ? + + : + '' + } +
+
+ ); + } +} + +``` + +- and new cases in the `memberReducer` to handle the cancel an loading_members indicator: + +_./src/reducers/memberReducer.ts_ + +```javascript +// ... +const handleMemberRequestCompletedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: false, members: action.members }); + return newState; +} + +const handleMemberRequestStartedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: true }); + return newState; +} + +const handleMemberRequestCancelledAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: false }); + return newState; +} + +export const memberReducer = (state: memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_STARTED: + return handleMemberRequestStartedAction(state, action); + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + case actionsEnums.MEMBER_REQUEST_CANCELLED: + return handleMemberRequestCancelledAction(state, action); + } + + return state; +}; + +``` diff --git a/17 Observables/src/actions/index.ts b/17 Observables/src/actions/index.ts index 13c8ce8..95891c2 100644 --- a/17 Observables/src/actions/index.ts +++ b/17 Observables/src/actions/index.ts @@ -1,4 +1,5 @@ import { memberRequestCompleted } from "./memberRequestCompleted"; import { memberRequest } from "./memberRequest"; +import { memberRequestCancelled } from "./memberRequestCancelled"; -export { memberRequest, memberRequestCompleted }; +export { memberRequest, memberRequestCompleted, memberRequestCancelled }; diff --git a/17 Observables/src/actions/memberRequestCancelled.ts b/17 Observables/src/actions/memberRequestCancelled.ts new file mode 100644 index 0000000..eb69b28 --- /dev/null +++ b/17 Observables/src/actions/memberRequestCancelled.ts @@ -0,0 +1,7 @@ +import { actionsEnums } from "../common/actionsEnums"; + +export const memberRequestCancelled = () => { + return { + type: actionsEnums.MEMBER_REQUEST_CANCELLED, + }; +}; diff --git a/17 Observables/src/common/actionsEnums.ts b/17 Observables/src/common/actionsEnums.ts index 6b91b69..6147002 100644 --- a/17 Observables/src/common/actionsEnums.ts +++ b/17 Observables/src/common/actionsEnums.ts @@ -3,4 +3,5 @@ export const actionsEnums = { UPDATE_USERPROFILE_FAVOURITE_COLOR: "UPDATE_USERPROFILE_FAVOURITE_COLOR", MEMBER_REQUEST_STARTED: "MEMBER_REQUEST_STARTED", MEMBER_REQUEST_COMPLETED: "MEMBER_REQUEST_COMPLETED", + MEMBER_REQUEST_CANCELLED: "MEMBER_REQUEST_CANCELLED", }; diff --git a/17 Observables/src/components/members/memberArea.tsx b/17 Observables/src/components/members/memberArea.tsx index 583a952..1873017 100644 --- a/17 Observables/src/components/members/memberArea.tsx +++ b/17 Observables/src/components/members/memberArea.tsx @@ -4,7 +4,9 @@ import { MemberEntity } from '../../model/member' interface Props { loadMembers: () => any; + cancelLoadMembers: () => any; members: Array; + members_loading: boolean; } export class MembersArea extends React.Component { @@ -19,12 +21,31 @@ export class MembersArea extends React.Component {

- this.props.loadMembers()} - /> +
+ + { + this.props.members_loading ? + + : + '' + } +
); } diff --git a/17 Observables/src/components/members/memberAreaContainer.ts b/17 Observables/src/components/members/memberAreaContainer.ts index ea41b4e..e2bc7f6 100644 --- a/17 Observables/src/components/members/memberAreaContainer.ts +++ b/17 Observables/src/components/members/memberAreaContainer.ts @@ -1,16 +1,18 @@ import { connect } from "react-redux"; -import { memberRequest } from "../../actions/memberRequest"; +import { memberRequest, memberRequestCancelled } from "../../actions/"; import { MembersArea } from "./memberArea"; const mapStateToProps = (state) => { return { - members: state.memberReducer.members + members: state.memberReducer.members, + members_loading: state.memberReducer.members_loading, }; } const mapDispatchToProps = (dispatch) => { return { - loadMembers: () => {return dispatch(memberRequest())} + loadMembers: () => {return dispatch(memberRequest())}, + cancelLoadMembers: () => {return dispatch(memberRequestCancelled())}, }; } diff --git a/17 Observables/src/epics/fetchMembersEpic.ts b/17 Observables/src/epics/fetchMembersEpic.ts index 0e30713..a55ddff 100644 --- a/17 Observables/src/epics/fetchMembersEpic.ts +++ b/17 Observables/src/epics/fetchMembersEpic.ts @@ -6,6 +6,8 @@ import { } from "rxjs/add/operator/mergeMap"; // map to throw a new action import { } from "rxjs/add/operator/map"; +import { } from "rxjs/add/operator/delay"; + import { actionsEnums } from "../common/actionsEnums"; import { memberRequestCompleted } from "../actions/"; import { memberAPI } from "../restApi/memberApi"; @@ -16,7 +18,10 @@ export const fetchMembersEpic = action$ => // to better understand the code action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => memberAPI.getAllMembers() + // Asynchronously wait 2000ms then continue + .delay(2000) // memberRequestCompleted will be only an action ({type: '...', ...}) // without "black magic" for promises .map(memberRequestCompleted) + .takeUntil(action$.ofType(actionsEnums.MEMBER_REQUEST_CANCELLED)) ); diff --git a/17 Observables/src/reducers/memberReducer.ts b/17 Observables/src/reducers/memberReducer.ts index d2126ca..f4f0b30 100644 --- a/17 Observables/src/reducers/memberReducer.ts +++ b/17 Observables/src/reducers/memberReducer.ts @@ -10,17 +10,30 @@ class memberState { } } +const handleMemberRequestCompletedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: false, members: action.members }); + return newState; +} + +const handleMemberRequestStartedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: true }); + return newState; +} + +const handleMemberRequestCancelledAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: false }); + return newState; +} + export const memberReducer = (state: memberState = new memberState(), action) => { switch (action.type) { + case actionsEnums.MEMBER_REQUEST_STARTED: + return handleMemberRequestStartedAction(state, action); case actionsEnums.MEMBER_REQUEST_COMPLETED: return handleMemberRequestCompletedAction(state, action); + case actionsEnums.MEMBER_REQUEST_CANCELLED: + return handleMemberRequestCancelledAction(state, action); } return state; }; - - -const handleMemberRequestCompletedAction = (state: memberState, action) => { - const newState = objectAssign({}, state, {members: action.members}); - return newState; -} diff --git a/17 Observables/src/store.ts b/17 Observables/src/store.ts index faf7326..c3a8d45 100644 --- a/17 Observables/src/store.ts +++ b/17 Observables/src/store.ts @@ -9,5 +9,6 @@ export const store = createStore( reducers, compose( applyMiddleware(epicMiddleware), + window['devToolsExtension'] ? window['devToolsExtension']() : f => f ), ); From 1c6765dcae7b5323ece730f5092b916dcc23adbe Mon Sep 17 00:00:00 2001 From: linuxonrails Date: Thu, 15 Dec 2016 13:39:44 +0100 Subject: [PATCH 5/5] Final fixes in 17 Observables --- 17 Observables/package.json | 2 +- 17 Observables/readme.md | 880 ++++++++++--------- 17 Observables/src/epics/fetchMembersEpic.ts | 2 + 17 Observables/src/reducers/memberReducer.ts | 2 + 4 files changed, 455 insertions(+), 431 deletions(-) diff --git a/17 Observables/package.json b/17 Observables/package.json index 9f5b8d6..5816863 100644 --- a/17 Observables/package.json +++ b/17 Observables/package.json @@ -4,7 +4,7 @@ "description": "In this sample we are going to setup the basic plumbing to \"build\" our project and launch it in a dev server.", "main": "index.js", "scripts": { - "build": "webpack", + "webpack": "webpack", "start": "webpack-dev-server --inline", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/17 Observables/readme.md b/17 Observables/readme.md index 395875b..d223945 100644 --- a/17 Observables/readme.md +++ b/17 Observables/readme.md @@ -34,586 +34,606 @@ Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0 or newer) if they are - Copy the content from _04 Refactor_ and execute: - ```bash - npm install - ``` + ```bash + npm install + ``` - We have to install libraries and typescript definitions to handle fetch calls: redux-observable, rx and rxjs -```bash -npm install --save-dev redux-observable rx rxjs @types/rx @types/es6-shim -``` + ```bash + npm install --save-dev redux-observable rx rxjs @types/rx @types/es6-shim + ``` - Let's register a Redux Epic Middleware in _./src/store.ts_ -```javascript -import { createStore, applyMiddleware, compose } from "redux"; -import { createEpicMiddleware } from "redux-observable"; -import { rootEpic } from "./epics"; -import { reducers } from "./reducers/"; + ```javascript + import { createStore, applyMiddleware, compose } from "redux"; + import { createEpicMiddleware } from "redux-observable"; + import { rootEpic } from "./epics"; + import { reducers } from "./reducers/"; -const epicMiddleware = createEpicMiddleware(rootEpic); + const epicMiddleware = createEpicMiddleware(rootEpic); -export const store = createStore( - reducers, - compose( - applyMiddleware(epicMiddleware), - ), -); + export const store = createStore( + reducers, + compose( + applyMiddleware(epicMiddleware), + ), + ); -``` + ``` - And use the store in _./src/main.tsx_ -```jsx -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { Provider } from "react-redux"; -import { App } from "./app"; -import { store } from "./store"; - -ReactDOM.render( - - - , - document.getElementById("root") -); + ```jsx + import * as React from "react"; + import * as ReactDOM from "react-dom"; + import { Provider } from "react-redux"; + import { App } from "./app"; + import { store } from "./store"; + + ReactDOM.render( + + + , + document.getElementById("root") + ); -``` + ``` - Let's create an entity under _./src/model/member.ts_ -```javascript -export class MemberEntity { - id: number; - login: string; - avatar_url: string; - - constructor() { - this.id = -1; - this.login = ""; - this.avatar_url = ""; + ```javascript + export class MemberEntity { + id: number; + login: string; + avatar_url: string; + + constructor() { + this.id = -1; + this.login = ""; + this.avatar_url = ""; + } } -} -``` + ``` - Let's create an epic to access this data, under _./src/epics/fetchMembersEpic.ts_ -```javascript -import 'rxjs'; + ```javascript + import 'rxjs'; -// merge all actions in only one action -import { } from "rxjs/add/operator/mergeMap"; + // merge all actions in only one action + import { } from "rxjs/add/operator/mergeMap"; -// map to throw a new action -import { } from "rxjs/add/operator/map"; + // map to throw a new action + import { } from "rxjs/add/operator/map"; -import { actionsEnums } from "../common/actionsEnums"; -import { memberRequestCompleted } from "../actions/"; -import { memberAPI } from "../restApi/memberApi"; + import { actionsEnums } from "../common/actionsEnums"; + import { memberRequestCompleted } from "../actions/"; + import { memberAPI } from "../restApi/memberApi"; -// the dollar symbol in the action$ param is just a convention -export const fetchMembersEpic = action$ => - // action param is not necesary, but it will be useful - // to better understand the code - action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => - memberAPI.getAllMembers() - // memberRequestCompleted will be only an action ({type: '...', ...}) - // without "black magic" for promises - .map(memberRequestCompleted) - ); + // the dollar symbol in the action$ param is just a convention + export const fetchMembersEpic = action$ => + // action param is not necesary, but it will be useful + // to better understand the code + action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => + memberAPI.getAllMembers() + // memberRequestCompleted will be only an action ({type: '...', ...}) + // without "black magic" for promises + .map(memberRequestCompleted) + ); -``` + ``` -_./src/epics/index.ts_ + _./src/epics/index.ts_ -```javascript -import { combineEpics } from "redux-observable"; + ```javascript + import { combineEpics } from "redux-observable"; -import { fetchMembersEpic } from "./fetchMembersEpic"; + import { fetchMembersEpic } from "./fetchMembersEpic"; -export const rootEpic = combineEpics(fetchMembersEpic); -``` + export const rootEpic = combineEpics(fetchMembersEpic); + ``` - Let's create a rest api class to access this data with rxjs-observable-ajax, under _./src/restApi/memberApi.ts_ -```javascript -import { ajax } from "rxjs/observable/dom/ajax"; - -// Sync mock data API, inspired from: -// https://gist.github.com/coryhouse/fd6232f95f9d601158e4 -class MemberAPI { - getAllMembers() { - return ( - ajax.getJSON("https://api.github.com/orgs/lemoncode/members") - ); + ```javascript + import { ajax } from "rxjs/observable/dom/ajax"; + + // Sync mock data API, inspired from: + // https://gist.github.com/coryhouse/fd6232f95f9d601158e4 + class MemberAPI { + getAllMembers() { + return ( + ajax.getJSON("https://api.github.com/orgs/lemoncode/members") + ); + } } -} -export const memberAPI = new MemberAPI(); -``` + export const memberAPI = new MemberAPI(); + ``` - Create _./src/actions/memberRequest.ts_ -```javascript -import { actionsEnums } from "../common/actionsEnums"; + ```javascript + import { actionsEnums } from "../common/actionsEnums"; -export const memberRequest = () => { - return { - type: actionsEnums.MEMBER_REQUEST_STARTED, + export const memberRequest = () => { + return { + type: actionsEnums.MEMBER_REQUEST_STARTED, + }; }; -}; -``` + ``` - It's time to define two new actions _./src/common/actionsEnums.ts_ -```javascript -export const actionsEnums = { - UPDATE_USERPROFILE_NAME: "UPDATE_USERPROFILE_NAME", - UPDATE_USERPROFILE_FAVOURITE_COLOR: "UPDATE_USERPROFILE_FAVOURITE_COLOR", - MEMBER_REQUEST_STARTED: "MEMBER_REQUEST_STARTED", - MEMBER_REQUEST_COMPLETED: "MEMBER_REQUEST_COMPLETED", -}; + ```javascript + export const actionsEnums = { + UPDATE_USERPROFILE_NAME: "UPDATE_USERPROFILE_NAME", + UPDATE_USERPROFILE_FAVOURITE_COLOR: "UPDATE_USERPROFILE_FAVOURITE_COLOR", + MEMBER_REQUEST_STARTED: "MEMBER_REQUEST_STARTED", + MEMBER_REQUEST_COMPLETED: "MEMBER_REQUEST_COMPLETED", + }; -``` + ``` - Let's create an *simple* action that will inform members once completed in _./src/actions/memberRequestCompleted.ts_ -```javascript -import { actionsEnums } from "../common/actionsEnums"; -import { MemberEntity } from "../model/member"; + ```javascript + import { actionsEnums } from "../common/actionsEnums"; + import { MemberEntity } from "../model/member"; -export const memberRequestCompleted = (members: MemberEntity[]) => { - // without "black magic" promises! MUAHAHAHAH! - return { - type: actionsEnums.MEMBER_REQUEST_COMPLETED, - members: members, + export const memberRequestCompleted = (members: MemberEntity[]) => { + // without "black magic" promises! MUAHAHAHAH! + return { + type: actionsEnums.MEMBER_REQUEST_COMPLETED, + members: members, + }; }; -}; -``` + ``` - And _./src/actions/index.ts_ to use easily: -```javascript -import { memberRequestCompleted } from "./memberRequestCompleted"; -import { memberRequest } from "./memberRequest"; + ```javascript + import { memberRequestCompleted } from "./memberRequestCompleted"; + import { memberRequest } from "./memberRequest"; -export { memberRequest, memberRequestCompleted }; -``` + export { memberRequest, memberRequestCompleted }; + ``` - Let's add a new reducer that will hold members state -_./src/reducers/memberReducer.ts_ + _./src/reducers/memberReducer.ts_ -```javascript -import { actionsEnums } from "../common/actionsEnums"; -import { MemberEntity } from "../model/member"; -import objectAssign = require("object-assign"); + ```javascript + import { actionsEnums } from "../common/actionsEnums"; + import { MemberEntity } from "../model/member"; + import objectAssign = require("object-assign"); -class memberState { - members: MemberEntity[]; + class memberState { + members: MemberEntity[]; - public constructor() { - this.members = []; + public constructor() { + this.members = []; + } } -} -export const memberReducer = (state: memberState = new memberState(), action) => { - switch (action.type) { - case actionsEnums.MEMBER_REQUEST_COMPLETED: - return handleMemberRequestCompletedAction(state, action); - } + export const memberReducer = (state: memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + } - return state; -}; + return state; + }; -const handleMemberRequestCompletedAction = (state: memberState, action) => { - const newState = objectAssign({}, state, {members: action.members}); - return newState; -} + const handleMemberRequestCompletedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, {members: action.members}); + return newState; + } -``` + ``` - Let's register it _./src/reducers/index.ts_ -```javascript -import { combineReducers } from 'redux'; -import { userProfileReducer } from './userProfile'; -import { memberReducer } from './memberReducer'; + ```javascript + import { combineReducers } from 'redux'; + import { userProfileReducer } from './userProfile'; + import { memberReducer } from './memberReducer'; -export const reducers = combineReducers({ - userProfileReducer, - memberReducer, -}); -``` + export const reducers = combineReducers({ + userProfileReducer, + memberReducer, + }); + ``` - Let's create a `memberRow` component _./src/components/members/memberRow.tsx_ -```jsx -import * as React from "react"; -import { MemberEntity } from "../../model/member"; - -interface Props { - member: MemberEntity; -} - -export const MemberRow = (props: Props) => { - return ( - - - - - - {props.member.id} - - - {props.member.login} - - - ); -} + ```jsx + import * as React from "react"; + import { MemberEntity } from "../../model/member"; + + interface Props { + member: MemberEntity; + } + + export const MemberRow = (props: Props) => { + return ( + + + + + + {props.member.id} + + + {props.member.login} + + + ); + } -``` + ``` - Let's create a memberTable component under _./src/components/members/memberTable.tsx_ -```jsx -import * as React from "react"; -import { MemberEntity } from "../../model/member"; -import { MemberRow } from "./memberRow"; - -interface Props { - members: MemberEntity[]; -} - -export const MembersTable = (props: Props) => { - return ( -
-

Members Page

- - - - - - - - - - { - props.members.map((member) => - - ) - } - -
- Avatar - - Id - - Name -
-
- ); -} + ```jsx + import * as React from "react"; + import { MemberEntity } from "../../model/member"; + import { MemberRow } from "./memberRow"; -``` + interface Props { + members: MemberEntity[]; + } -- Let's create a memberArea component (include a load button) in _./src/components/members/memberArea.tsx_ + export const MembersTable = (props: Props) => { + return ( +
+

Members Page

+ + + + + + + + + + { + props.members.map((member) => + + ) + } + +
+ Avatar + + Id + + Name +
+
+ ); + } -```jsx -import * as React from 'react'; -import { MembersTable } from './memberTable'; -import { MemberEntity } from '../../model/member' + ``` -interface Props { - loadMembers: () => any; - members: Array; -} +- Let's create a memberArea component (include a load button) in _./src/components/members/memberArea.tsx_ -export class MembersArea extends React.Component { - constructor(props: Props){ - super(props); + ```jsx + import * as React from 'react'; + import { MembersTable } from './memberTable'; + import { MemberEntity } from '../../model/member' - this.state = {members:[]}; + interface Props { + loadMembers: () => any; + members: Array; } - render(){ - return ( -
- -
- this.props.loadMembers()} - /> -
- ); + export class MembersArea extends React.Component { + constructor(props: Props){ + super(props); + + this.state = {members:[]}; + } + + render(){ + return ( +
+ +
+ this.props.loadMembers()} + /> +
+ ); + } } -} -``` + ``` - Let's create a memberAreaContainer. -_./src/components/members/memberAreaContainer.ts_ + _./src/components/members/memberAreaContainer.ts_ -```javascript -import { connect } from "react-redux"; -import { memberRequest } from "../../actions/memberRequest"; -import { MembersArea } from "./memberArea"; + ```javascript + import { connect } from "react-redux"; + import { memberRequest } from "../../actions/memberRequest"; + import { MembersArea } from "./memberArea"; -const mapStateToProps = (state) => { - return { - members: state.memberReducer.members - }; -} + const mapStateToProps = (state) => { + return { + members: state.memberReducer.members + }; + } -const mapDispatchToProps = (dispatch) => { - return { - loadMembers: () => {return dispatch(memberRequest())} - }; -} + const mapDispatchToProps = (dispatch) => { + return { + loadMembers: () => {return dispatch(memberRequest())} + }; + } -export const MembersAreaContainer = connect( - mapStateToProps, - mapDispatchToProps, -)(MembersArea) + export const MembersAreaContainer = connect( + mapStateToProps, + mapDispatchToProps, + )(MembersArea) -``` + ``` - Let's create an _./src/components/members/index.ts_ -```javascript -import { MembersAreaContainer } from './memberAreaContainer'; + ```javascript + import { MembersAreaContainer } from './memberAreaContainer'; -export { - MembersAreaContainer -} + export { + MembersAreaContainer + } -``` + ``` - Let's instantiate it on _app.tsx_ -```jsx -import * as React from "react"; -import { MembersAreaContainer } from './components/members'; -import { HelloWorldContainer } from "./components/helloworld"; -import { NameEditContainer } from "./components/nameEdit"; -import { ColorDisplayerContainer } from "./components/color"; -import { ColorPickerContainer } from "./components/color"; - -export const App = () => { - return ( -
- -
- -
- -
- -
- -
- ); -}; + ```jsx + import * as React from "react"; + import { MembersAreaContainer } from './components/members'; + import { HelloWorldContainer } from "./components/helloworld"; + import { NameEditContainer } from "./components/nameEdit"; + import { ColorDisplayerContainer } from "./components/color"; + import { ColorPickerContainer } from "./components/color"; -``` + export const App = () => { + return ( +
+ +
+ +
+ +
+ +
+ +
+ ); + }; -- Let's give a try. + ``` -```shell -npm start -``` +- Let's give a try. ------------------------------------------------------------------------------------------------------------------------------------------------- + ```shell + npm start + ``` ## Additional step - We will add DevToolsExtensions support to see better the behavior: -_./src/store.ts_ + _./src/store.ts_ -```javascript - // ... - reducers, - compose( - applyMiddleware(epicMiddleware), - window['devToolsExtension'] ? window['devToolsExtension']() : f => f - ), -); -``` + ```javascript + // ... + reducers, + compose( + applyMiddleware(epicMiddleware), + window['devToolsExtension'] ? window['devToolsExtension']() : f => f + ), + ); + ``` - We will add a delay in the ajax request and cancel behavior: -_./src/common/actionsEnums.ts_ + _./src/common/actionsEnums.ts_ -```javascript -export const actionsEnums = { - // ... - MEMBER_REQUEST_CANCELLED: "MEMBER_REQUEST_CANCELLED", -}; -``` + ```javascript + export const actionsEnums = { + // ... + MEMBER_REQUEST_CANCELLED: "MEMBER_REQUEST_CANCELLED", + }; + ``` -_./src/actions/memberRequestCancelled.ts_ + _./src/actions/memberRequestCancelled.ts_ -```javascript -import { actionsEnums } from "../common/actionsEnums"; + ```javascript + import { actionsEnums } from "../common/actionsEnums"; -export const memberRequestCancelled = () => { - return { - type: actionsEnums.MEMBER_REQUEST_CANCELLED, + export const memberRequestCancelled = () => { + return { + type: actionsEnums.MEMBER_REQUEST_CANCELLED, + }; }; -}; -``` + ``` -_./src/actions/index.ts_ + _./src/actions/index.ts_ -```javascript -import { memberRequestCompleted } from "./memberRequestCompleted"; -import { memberRequest } from "./memberRequest"; -import { memberRequestCancelled } from "./memberRequestCancelled"; + ```javascript + import { memberRequestCompleted } from "./memberRequestCompleted"; + import { memberRequest } from "./memberRequest"; + import { memberRequestCancelled } from "./memberRequestCancelled"; -export { memberRequest, memberRequestCompleted, memberRequestCancelled }; -``` + export { memberRequest, memberRequestCompleted, memberRequestCancelled }; + ``` -_./src/epics/fetchMembersEpic.ts_ + _./src/epics/fetchMembersEpic.ts_ -```javascript -// ... + ```javascript + // ... -import { } from "rxjs/add/operator/delay"; + // delay the getAllMembers ajax petition + import { } from "rxjs/add/operator/delay"; -// ... -export const fetchMembersEpic = action$ => // ... - action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => - memberAPI.getAllMembers() - // Asynchronously wait 2000ms then continue - .delay(2000) - // memberRequestCompleted will be only an action ({type: '...', ...}) - // without "black magic" for promises - .map(memberRequestCompleted) - .takeUntil(action$.ofType(actionsEnums.MEMBER_REQUEST_CANCELLED)) - ); + export const fetchMembersEpic = action$ => + // ... + action$.ofType(actionsEnums.MEMBER_REQUEST_STARTED).mergeMap(action => + memberAPI.getAllMembers() + // Asynchronously wait 2000ms then continue + .delay(2000) + // memberRequestCompleted will be only an action ({type: '...', ...}) + // without "black magic" for promises + .map(memberRequestCompleted) + .takeUntil(action$.ofType(actionsEnums.MEMBER_REQUEST_CANCELLED)) + ); -``` + ``` - We add a button to cancel the `MEMBER_REQUEST_STARTED` action and a members_loading indicator in state: -_./src/components/members/memberAreaContainer.ts_ + _./src/components/members/memberAreaContainer.ts_ -```javascript -import { connect } from "react-redux"; -import { memberRequest, memberRequestCancelled } from "../../actions/"; -import { MembersArea } from "./memberArea"; - -const mapStateToProps = (state) => { - return { - members: state.memberReducer.members, - members_loading: state.memberReducer.members_loading, - }; -} + ```javascript + import { connect } from "react-redux"; + import { memberRequest, memberRequestCancelled } from "../../actions/"; + import { MembersArea } from "./memberArea"; -const mapDispatchToProps = (dispatch) => { - return { - loadMembers: () => {return dispatch(memberRequest())}, - cancelLoadMembers: () => {return dispatch(memberRequestCancelled())}, - }; -} + const mapStateToProps = (state) => { + return { + members: state.memberReducer.members, + members_loading: state.memberReducer.members_loading, + }; + } -export const MembersAreaContainer = connect( - mapStateToProps, - mapDispatchToProps, -)(MembersArea) -``` + const mapDispatchToProps = (dispatch) => { + return { + loadMembers: () => {return dispatch(memberRequest())}, + cancelLoadMembers: () => {return dispatch(memberRequestCancelled())}, + }; + } -```jsx -// ... + export const MembersAreaContainer = connect( + mapStateToProps, + mapDispatchToProps, + )(MembersArea) + ``` -interface Props { - loadMembers: () => any; - cancelLoadMembers: () => any; - members: Array; - members_loading: boolean; -} + _./src/components/members/memberArea.tsx_ -export class MembersArea extends React.Component { + ```jsx // ... - render(){ - return ( -
- -
-
- { this.props.members_loading ? - 'loading...' + : - 'load' + '' } - - { - this.props.members_loading ? - - : - '' - } +
- - ); + ); + } } -} -``` + ``` - and new cases in the `memberReducer` to handle the cancel an loading_members indicator: -_./src/reducers/memberReducer.ts_ - -```javascript -// ... -const handleMemberRequestCompletedAction = (state: memberState, action) => { - const newState = objectAssign({}, state, { members_loading: false, members: action.members }); - return newState; -} - -const handleMemberRequestStartedAction = (state: memberState, action) => { - const newState = objectAssign({}, state, { members_loading: true }); - return newState; -} - -const handleMemberRequestCancelledAction = (state: memberState, action) => { - const newState = objectAssign({}, state, { members_loading: false }); - return newState; -} - -export const memberReducer = (state: memberState = new memberState(), action) => { - switch (action.type) { - case actionsEnums.MEMBER_REQUEST_STARTED: - return handleMemberRequestStartedAction(state, action); - case actionsEnums.MEMBER_REQUEST_COMPLETED: - return handleMemberRequestCompletedAction(state, action); - case actionsEnums.MEMBER_REQUEST_CANCELLED: - return handleMemberRequestCancelledAction(state, action); + _./src/reducers/memberReducer.ts_ + + ```javascript + import { actionsEnums } from "../common/actionsEnums"; + import { MemberEntity } from "../model/member"; + import objectAssign = require("object-assign"); + + class memberState { + members: MemberEntity[]; + members_loading: boolean; + + public constructor() { + this.members = []; + this.members_loading = false; + } + } + + const handleMemberRequestCompletedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: false, members: action.members }); + return newState; + } + + const handleMemberRequestStartedAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: true }); + return newState; + } + + const handleMemberRequestCancelledAction = (state: memberState, action) => { + const newState = objectAssign({}, state, { members_loading: false }); + return newState; } - return state; -}; + export const memberReducer = (state: memberState = new memberState(), action) => { + switch (action.type) { + case actionsEnums.MEMBER_REQUEST_STARTED: + return handleMemberRequestStartedAction(state, action); + case actionsEnums.MEMBER_REQUEST_COMPLETED: + return handleMemberRequestCompletedAction(state, action); + case actionsEnums.MEMBER_REQUEST_CANCELLED: + return handleMemberRequestCancelledAction(state, action); + } + + return state; + }; + + ``` + +- Let's give a try again. We should can cancel the loading of members with a new button. -``` + ```shell + npm start + ``` diff --git a/17 Observables/src/epics/fetchMembersEpic.ts b/17 Observables/src/epics/fetchMembersEpic.ts index a55ddff..4bae8d5 100644 --- a/17 Observables/src/epics/fetchMembersEpic.ts +++ b/17 Observables/src/epics/fetchMembersEpic.ts @@ -1,11 +1,13 @@ import 'rxjs'; + // merge all actions in only one action import { } from "rxjs/add/operator/mergeMap"; // map to throw a new action import { } from "rxjs/add/operator/map"; +// delay the getAllMembers ajax petition import { } from "rxjs/add/operator/delay"; import { actionsEnums } from "../common/actionsEnums"; diff --git a/17 Observables/src/reducers/memberReducer.ts b/17 Observables/src/reducers/memberReducer.ts index f4f0b30..f093fb5 100644 --- a/17 Observables/src/reducers/memberReducer.ts +++ b/17 Observables/src/reducers/memberReducer.ts @@ -4,9 +4,11 @@ import objectAssign = require("object-assign"); class memberState { members: MemberEntity[]; + members_loading: boolean; public constructor() { this.members = []; + this.members_loading = false; } }