diff --git a/17 Observables/package.json b/17 Observables/package.json
new file mode 100644
index 0000000..5816863
--- /dev/null
+++ b/17 Observables/package.json
@@ -0,0 +1,42 @@
+{
+ "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": {
+ "webpack": "webpack",
+ "start": "webpack-dev-server --inline",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@types/es6-shim": "^0.31.32",
+ "@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/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..d223945
--- /dev/null
+++ b/17 Observables/readme.md
@@ -0,0 +1,639 @@
+# 16 Observables
+
+This sample takes as starting point _04 Refactor_
+
+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.
+
+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.
+
+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.
+
+> 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
+
+- 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 @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/";
+ 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)
+ );
+
+ ```
+
+ _./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";
+
+ // 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();
+ ```
+
+- Create _./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/memberRequestCompleted.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_
+
+ ```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
+
+
+
+ |
+ Avatar
+ |
+
+ Id
+ |
+
+ Name
+ |
+
+
+
+ {
+ props.members.map((member) =>
+
+ )
+ }
+
+
+
+ );
+ }
+
+ ```
+
+- Let's create a memberArea component (include a load button) in _./src/components/members/memberArea.tsx_
+
+ ```jsx
+ 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()}
+ />
+
+ );
+ }
+ }
+
+ ```
+
+- Let's create a memberAreaContainer.
+
+ _./src/components/members/memberAreaContainer.ts_
+
+ ```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_
+
+ ```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
+ ```
+
+## 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
+ // ...
+
+ // 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))
+ );
+
+ ```
+
+- 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)
+ ```
+
+ _./src/components/members/memberArea.tsx_
+
+ ```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
+ 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;
+ }
+
+ 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/actions/index.ts b/17 Observables/src/actions/index.ts
new file mode 100644
index 0000000..95891c2
--- /dev/null
+++ b/17 Observables/src/actions/index.ts
@@ -0,0 +1,5 @@
+import { memberRequestCompleted } from "./memberRequestCompleted";
+import { memberRequest } from "./memberRequest";
+import { memberRequestCancelled } from "./memberRequestCancelled";
+
+export { memberRequest, memberRequestCompleted, memberRequestCancelled };
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/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/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..b35ac19
--- /dev/null
+++ b/17 Observables/src/app.tsx
@@ -0,0 +1,22 @@
+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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/17 Observables/src/common/actionsEnums.ts b/17 Observables/src/common/actionsEnums.ts
new file mode 100644
index 0000000..6147002
--- /dev/null
+++ b/17 Observables/src/common/actionsEnums.ts
@@ -0,0 +1,7 @@
+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",
+ MEMBER_REQUEST_CANCELLED: "MEMBER_REQUEST_CANCELLED",
+};
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..13cb8e3
--- /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/memberArea.tsx b/17 Observables/src/components/members/memberArea.tsx
new file mode 100644
index 0000000..1873017
--- /dev/null
+++ b/17 Observables/src/components/members/memberArea.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import { MembersTable } from './memberTable';
+import { MemberEntity } from '../../model/member'
+
+interface Props {
+ loadMembers: () => any;
+ cancelLoadMembers: () => any;
+ members: Array;
+ members_loading: boolean;
+}
+
+export class MembersArea extends React.Component {
+ constructor(props: Props){
+ super(props);
+
+ this.state = {members:[]};
+ }
+
+ render(){
+ return (
+
+
+
+
+
+ {
+ this.props.members_loading ?
+
+ :
+ ''
+ }
+
+
+ );
+ }
+}
diff --git a/17 Observables/src/components/members/memberAreaContainer.ts b/17 Observables/src/components/members/memberAreaContainer.ts
new file mode 100644
index 0000000..e2bc7f6
--- /dev/null
+++ b/17 Observables/src/components/members/memberAreaContainer.ts
@@ -0,0 +1,22 @@
+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)
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
+
+
+
+ |
+ Avatar
+ |
+
+ Id
+ |
+
+ Name
+ |
+
+
+
+ {
+ props.members.map((member) =>
+
+ )
+ }
+
+
+
+ );
+}
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..4bae8d5
--- /dev/null
+++ b/17 Observables/src/epics/fetchMembersEpic.ts
@@ -0,0 +1,29 @@
+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";
+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()
+ // 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/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..9ec1172
--- /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..f093fb5
--- /dev/null
+++ b/17 Observables/src/reducers/memberReducer.ts
@@ -0,0 +1,41 @@
+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;
+}
+
+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/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..c3a8d45
--- /dev/null
+++ b/17 Observables/src/store.ts
@@ -0,0 +1,14 @@
+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),
+ window['devToolsExtension'] ? window['devToolsExtension']() : f => f
+ ),
+);
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
+ })
+ ]
+}