diff --git a/package-lock.json b/package-lock.json index bfef477..c3a36d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,14 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/runtime": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", + "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, "@microsoft/load-themed-styles": { "version": "1.8.54", "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.8.54.tgz", @@ -83,6 +91,25 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.0.0.tgz", + "integrity": "sha512-p5/vXqS4+Gq7EYzNv4SMi/yy7cM/BUbZORlPXY6F8wCyo5EfPa3Gpf6CEQX+UtbK5g2mqgERqVYyutzZYKIpaQ==", + "dev": true, + "requires": { + "@types/react": "*", + "redux": "^4.0.0" + } + }, + "@types/redux": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz", + "integrity": "sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=", + "dev": true, + "requires": { + "redux": "*" + } + }, "@uifabric/azure-themes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@uifabric/azure-themes/-/azure-themes-0.1.1.tgz", @@ -3024,6 +3051,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", + "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", + "requires": { + "react-is": "^16.3.2" + } + }, "homedir-polyfill": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", @@ -3288,6 +3323,14 @@ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "dev": true }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", @@ -4628,6 +4671,24 @@ "scheduler": "^0.12.0" } }, + "react-is": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz", + "integrity": "sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==" + }, + "react-redux": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz", + "integrity": "sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==", + "requires": { + "@babel/runtime": "^7.2.0", + "hoist-non-react-statics": "^3.2.1", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.3" + } + }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", @@ -4654,6 +4715,20 @@ "readable-stream": "^2.0.2" } }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -5393,6 +5468,11 @@ "has-flag": "^3.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "tapable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", diff --git a/package.json b/package.json index ca7f2ca..f77754d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "devDependencies": { "@types/react": "^16.7.20", "@types/react-dom": "^16.0.11", + "@types/redux": "^3.6.0", + "@types/react-redux": "^7.0.0", "html-webpack-plugin": "^3.2.0", "ts-loader": "^5.3.3", "typescript": "^3.2.4", @@ -24,6 +26,8 @@ "office-ui-fabric-react": "^6.128.0", "@uifabric/experiments": "^6.51.1", "react": "^16.7.0", - "react-dom": "^16.7.0" + "react-dom": "^16.7.0", + "redux": "^4.0.1", + "react-redux": "^6.0.0" } } diff --git a/playground/src/actions/index.ts b/playground/src/actions/index.ts new file mode 100644 index 0000000..08da3c5 --- /dev/null +++ b/playground/src/actions/index.ts @@ -0,0 +1,15 @@ +import { Action, ActionCreator } from 'redux'; + +export type ActionTypes = 'add' | 'remove' | 'edit' | 'complete' | 'completeAll' | 'clear' | 'filter'; +type TodoActionCreator = ActionCreator>; +export interface TodoAction extends Action { + [extraProps: string]: any; +} + +export const add = (label: string): TodoAction => ({ type: 'add', label }); +export const remove = (id: string): TodoAction => ({ type: 'remove', id }); +export const edit = (id: string, label: string): TodoAction => ({ type: 'edit', id, label }); +export const complete = (id: string): TodoAction => ({ type: 'complete', id }); +export const completeAll = (): TodoAction => ({ type: 'completeAll' }); +export const clear = (): TodoAction => ({ type: 'clear' }); +export const filter = (filterTypes: string): TodoAction => ({ type: 'filter', filterTypes }); diff --git a/playground/src/components/TodoApp.tsx b/playground/src/components/TodoApp.tsx index c900908..b66911c 100644 --- a/playground/src/components/TodoApp.tsx +++ b/playground/src/components/TodoApp.tsx @@ -1,17 +1,24 @@ import React from 'react'; -import { Stack, Text } from '@uifabric/experiments'; -import { TodoList } from './TodoList'; +import { Stack } from '@uifabric/experiments'; import { TodoFooter } from './TodoFooter'; -import { Pivot, PivotItem } from 'office-ui-fabric-react'; import { TodoHeader } from './TodoHeader'; +import { TodoList } from './TodoList'; +import { TodoItem, FilterTypes } from '../store'; -export class TodoApp extends React.Component { +export interface TodoAppProps { + todos: { [id: string]: TodoItem }; + filter: FilterTypes; +} + +export class TodoApp extends React.Component { render() { + const { todos, filter } = this.props; + return ( - + diff --git a/playground/src/components/TodoAppContainer.tsx b/playground/src/components/TodoAppContainer.tsx new file mode 100644 index 0000000..1b366d0 --- /dev/null +++ b/playground/src/components/TodoAppContainer.tsx @@ -0,0 +1,24 @@ +import * as actions from '../actions'; +import { Store } from '../store'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { TodoApp } from './TodoApp'; + +export function mapStateToProps({ todos, filter }: Store) { + return { + todos, + filter + }; +} + +export function mapDispatchToProps(dispatch: Dispatch) { + return { + add: (label: string) => dispatch(actions.add(label)), + remove: (id: string) => dispatch(actions.remove(id)) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TodoApp); diff --git a/playground/src/components/TodoFooter.tsx b/playground/src/components/TodoFooter.tsx index 237f850..3dc012f 100644 --- a/playground/src/components/TodoFooter.tsx +++ b/playground/src/components/TodoFooter.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Text, Stack } from '@uifabric/experiments'; -import { Checkbox, Button, Pivot, PivotItem } from 'office-ui-fabric-react'; export interface TodoFooterProps {} diff --git a/playground/src/components/TodoHeader.tsx b/playground/src/components/TodoHeader.tsx index 99aee4f..1aec605 100644 --- a/playground/src/components/TodoHeader.tsx +++ b/playground/src/components/TodoHeader.tsx @@ -8,7 +8,7 @@ export const TodoHeader = (props: TodoFooterProps) => { return ( - Yet Another To Do Example Application + todos diff --git a/playground/src/components/TodoList.tsx b/playground/src/components/TodoList.tsx index 5b0747c..50b2cb1 100644 --- a/playground/src/components/TodoList.tsx +++ b/playground/src/components/TodoList.tsx @@ -1,16 +1,22 @@ import React from 'react'; import { Stack } from '@uifabric/experiments'; import { TodoListItem } from './TodoListItem'; -import { Pivot, PivotItem } from 'office-ui-fabric-react'; +import { TodoItem, FilterTypes } from '../store'; -export class TodoList extends React.Component { +export interface TodoListProps { + todos: { [id: string]: TodoItem }; + filter: FilterTypes; +} + +export class TodoList extends React.Component { render() { + const { filter, todos } = this.props; return ( - - - - + {Object.keys(todos).map(id => { + const todo = todos[id]; + return ; + })} ); } diff --git a/playground/src/index.tsx b/playground/src/index.tsx index b99b0c1..a36ffae 100644 --- a/playground/src/index.tsx +++ b/playground/src/index.tsx @@ -1,5 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { TodoApp } from './components/TodoApp'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; +import { reducer } from './reducers'; +import TodoAppContainer from './components/TodoAppContainer'; -ReactDOM.render(, document.getElementById('app')); +const store = createStore(reducer); + +ReactDOM.render( + + + , + document.getElementById('app') +); diff --git a/playground/src/reducers/createReducer.ts b/playground/src/reducers/createReducer.ts new file mode 100644 index 0000000..24ea0e0 --- /dev/null +++ b/playground/src/reducers/createReducer.ts @@ -0,0 +1,15 @@ +import { Reducer } from 'redux'; +import { ActionTypes, TodoAction } from '../actions'; + +export function createReducer( + initialState: T, + handlers: { [actionType in ActionTypes]?: (state: T, action: TodoAction) => T } +): Reducer { + return function reducer(state = initialState, action: TodoAction): T { + if (handlers.hasOwnProperty(action.type)) { + return handlers[action.type](state, action); + } else { + return state; + } + }; +} diff --git a/playground/src/reducers/index.ts b/playground/src/reducers/index.ts new file mode 100644 index 0000000..3d7435e --- /dev/null +++ b/playground/src/reducers/index.ts @@ -0,0 +1,22 @@ +import { createReducer } from './createReducer'; +import { Store, FilterTypes } from '../store'; +import { combineReducers } from 'redux'; + +let counter = 0; + +export const reducer = combineReducers({ + todos: createReducer( + {}, + { + add(state, action) { + const id = String(counter++); + return { ...state, [id]: { label: action.label, completed: false } }; + } + } + ), + filter: createReducer('all', { + filter(state, action) { + return action.filter; + } + }) +}); diff --git a/playground/src/store/index.ts b/playground/src/store/index.ts new file mode 100644 index 0000000..221b5f4 --- /dev/null +++ b/playground/src/store/index.ts @@ -0,0 +1,14 @@ +export type FilterTypes = 'all' | 'active' | 'completed'; + +export interface TodoItem { + label: string; + completed: boolean; +} + +export interface Store { + todos: { + [id: string]: TodoItem; + }; + + filter: FilterTypes; +}