diff --git a/package.json b/package.json index ea6c337..3d9296b 100644 --- a/package.json +++ b/package.json @@ -8,22 +8,24 @@ "url": "git@github.com:Indoqa/indoqa-react-redux.git" }, "scripts": { - "start": "indoqa-dev-server ./indoqa-webpack-options.js", + "analyze": "source-map-explorer ./target/assets/app-*", "build": "indoqa-webpack ./indoqa-webpack-options.js", "docs": "indoqa-webpack ./indoqa-webpack-docs.js", "flow": "flow", "lint": "indoqa-eslint ./src/main", "package": "yarn run lint && yarn run flow && yarn run test && indoqa-webpack ./indoqa-webpack-options.js", + "postinstall": "yarn run package", + "start": "indoqa-dev-server ./indoqa-webpack-options.js", "test": "indoqa-jest", "test:watch": "yarn run test --watch", - "test:coverage": "yarn run test --coverage", - "postinstall": "yarn run package" + "test:coverage": "yarn run test --coverage" }, "dependencies": { "fela": "6.1.7", "fela-monolithic": "5.0.21", "fela-plugin-named-media-query": "5.0.13", "fela-preset-web": "8.0.5", + "formik": "0.11.11", "indoqa-react-app": "2.6.0", "indoqa-react-fela": "0.6.0", "ramda": "0.25.0", @@ -37,11 +39,14 @@ "redux-logger": "3.0.6", "redux-observable": "0.18.0", "reselect": "3.0.1", - "rxjs": "5.5.10" + "rxjs": "5.5.10", + "shortid": "2.2.8", + "yup": "0.24.1" }, "devDependencies": { "flow-bin": "0.70.0", - "indoqa-webpack": "1.2.0" + "indoqa-webpack": "1.2.0", + "source-map-explorer": "1.5.0" }, "proxy": { "/geonames": { diff --git a/src/main/app/rootEpic.js b/src/main/app/rootEpic.js index 1e00360..6a5ee1c 100644 --- a/src/main/app/rootEpic.js +++ b/src/main/app/rootEpic.js @@ -1,10 +1,13 @@ // @flow import {ajax} from 'rxjs/observable/dom/ajax' import {combineEpics} from 'redux-observable' + +import formsEpics from '../forms/store/forms.epics' import timeEpics from '../time/store/time.epics.js' import wordsEpics from '../words/store/words.epics' const combinedEpics = combineEpics( + ...formsEpics, ...timeEpics, ...wordsEpics ) diff --git a/src/main/app/rootReducer.js b/src/main/app/rootReducer.js index ad2cef6..fbf2778 100644 --- a/src/main/app/rootReducer.js +++ b/src/main/app/rootReducer.js @@ -1,11 +1,13 @@ // @flow import {combineReducersWithRouter} from 'indoqa-react-app' -import time from '../time/store/time.reducer.js' -import todos from '../todos/store/todos.reducer.js' +import forms from '../forms/store/forms.reducer' +import time from '../time/store/time.reducer' +import todos from '../todos/store/todos.reducer' import words from '../words/store/words.reducer' const reducers = { + forms, time, todos, words, diff --git a/src/main/app/routes.react.js b/src/main/app/routes.react.js index 2eb7fcf..b579d89 100644 --- a/src/main/app/routes.react.js +++ b/src/main/app/routes.react.js @@ -3,15 +3,20 @@ import React from 'react' import {IndexRoute, Route} from 'react-router' import App from './App.react.js' +import FormsPage from '../forms/components/FormsPage.redux.js' import TimePage from '../time/components/TimePage.react.js' import TodosPage from '../todos/components/TodosPage.react.js' -import WordsPage from '../words/components/WordsPage.react' +import UserPage from '../forms/components/UserPage.redux.js' +import WordsPage from '../words/components/WordsPage.react.js' const routes = ( + + + - + ) diff --git a/src/main/app/selectors.js b/src/main/app/selectors.js index 622db73..5320145 100644 --- a/src/main/app/selectors.js +++ b/src/main/app/selectors.js @@ -1,14 +1,17 @@ // @flow +import type {FormsState} from '../forms/types/FormsState' import type {TimeState} from '../time/types/TimeState' import type {TodoState} from '../todos/types/TodoState' import type {WordsState} from '../words/types/WordsState' type State = { + forms: FormsState, time: TimeState, todos: TodoState, words: WordsState, } +export const selectFormsState = (state: State) => state.forms export const selectTimeState = (state: State) => state.time export const selectTodoState = (state: State) => state.todos export const selectWordsState = (state: State) => state.words diff --git a/src/main/app/theme.js b/src/main/app/theme.js index ea9ae54..cca0b67 100644 --- a/src/main/app/theme.js +++ b/src/main/app/theme.js @@ -3,7 +3,7 @@ export default { colors: { text: '#030303', disabled: '#727272', - bgLight: '#e5e5e5', + bgLight: '#d5d5d5', }, fonts: { text: 'sans-serif', diff --git a/src/main/commons/components/atoms/ButtonLink.react.js b/src/main/commons/components/atoms/ButtonLink.react.js new file mode 100644 index 0000000..0d70921 --- /dev/null +++ b/src/main/commons/components/atoms/ButtonLink.react.js @@ -0,0 +1,13 @@ +// @flow +import {createComponentWithProxy} from 'react-fela' + +const ButtonLink = ({theme}) => ({ + '& > a': { + color: theme.colors.text, + display: 'block', + height: '100%', + textDecoration: 'none', + }, +}) + +export default createComponentWithProxy(ButtonLink, 'button') diff --git a/src/main/commons/components/molecules/Logo.react.js b/src/main/commons/components/molecules/Logo.react.js index 3615189..9776bfd 100644 --- a/src/main/commons/components/molecules/Logo.react.js +++ b/src/main/commons/components/molecules/Logo.react.js @@ -1,14 +1,19 @@ // @flow import {createComponentWithProxy} from 'react-fela' +import {Box} from 'indoqa-react-fela' const Logo = ({theme}) => { return ({ - height: '50px', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', - padding: theme.spacing.space1, + height: 50, + fontWeight: 'bold', + '> a': { + textDecoration: 'none', + color: theme.colors.text, + }, }) } -export default createComponentWithProxy(Logo, 'div') +export default createComponentWithProxy(Logo, Box) diff --git a/src/main/commons/components/organisms/MainMenu.react.js b/src/main/commons/components/organisms/MainMenu.react.js index 513afd0..3cf9c63 100644 --- a/src/main/commons/components/organisms/MainMenu.react.js +++ b/src/main/commons/components/organisms/MainMenu.react.js @@ -9,10 +9,10 @@ import MenuLink from '../molecules/MenuLink.react.js' const MainMenu = () => ( - - INDOQA-REACT-REDUX + + INDOQA: React-Redux samples - + Time @@ -22,6 +22,9 @@ const MainMenu = () => ( Words + + Forms + ) diff --git a/src/main/commons/components/templates/MainMenuTemplate.react.js b/src/main/commons/components/templates/MainMenuTemplate.react.js index 6ebd13e..9920b69 100644 --- a/src/main/commons/components/templates/MainMenuTemplate.react.js +++ b/src/main/commons/components/templates/MainMenuTemplate.react.js @@ -16,7 +16,7 @@ const MainMenuTemplate = ({title, header, children}: Props) => ( - + {title} {header} diff --git a/src/main/forms/components/AddressesForm.react.js b/src/main/forms/components/AddressesForm.react.js new file mode 100644 index 0000000..ef27a69 --- /dev/null +++ b/src/main/forms/components/AddressesForm.react.js @@ -0,0 +1,83 @@ +// @flow +import * as React from 'react' +import {FieldArray} from 'formik' +import {Box, Flex, Text} from 'indoqa-react-fela' + +import {createNewAddress} from '../store/forms.factory' +import FormRow from './FormRow.react' + +type Props = { + values: any, + errors: any, + touched: any, +} + +const renderAddressHeader = (arrayHelpers, count, index) => { + return ( + + Address {index + 1} + + {index < count - 1 ? + + : null + } + {index > 0 ? + + : null + } + + ) +} + +const renderAddressForm = (arrayHelpers, values, errors, touched, address, index) => { + return ( + + + {renderAddressHeader(arrayHelpers, values.length, index)} + + + + + + + + ) +} + +const renderAddressesHeader = (arrayHelpers) => { + return ( + + + Addresses + + + + ) +} + +const renderForms = (arrayHelpers, values, errors, touched) => { + const {addresses} = values + + if (!(addresses && addresses.length > 0)) { + return null + } + return addresses.map((address, index) => ( + renderAddressForm(arrayHelpers, values, errors, touched, address, index) + )) +} + +const AddressesForm = ({values, errors, touched}:Props) => { + return ( + ( + + {renderAddressesHeader(arrayHelpers)} + {renderForms(arrayHelpers, values, errors, touched)} + + )} + /> + ) +} + +export default AddressesForm diff --git a/src/main/forms/components/FormRow.react.js b/src/main/forms/components/FormRow.react.js new file mode 100644 index 0000000..465f5a2 --- /dev/null +++ b/src/main/forms/components/FormRow.react.js @@ -0,0 +1,81 @@ +// @flow +import * as React from 'react' +import {createComponent, createComponentWithProxy} from 'react-fela' +import {Field, getIn} from 'formik' +import {Text} from 'indoqa-react-fela' + +type Props = { + name: string, + label: string, + errors: any, + touched: any, +} + +const RowContainer = createComponent(() => ({ + marginTop: 3, + marginBottom: 3, +})) + +const Label = createComponent(() => ({ + display: 'inline-block', + width: '100', +}), 'label') + +const InputField = createComponentWithProxy(({hasError}) => ({ + borderStyle: 'solid', + borderWidth: 1, + padding: 4, + borderColor: hasError ? 'red' : 'grey', + outline: 'none', + boxShadow: 'none', + transition: 'all 0.30s ease-in-out', + ':focus': { + boxShadow: '0 0 5px rgba(81, 203, 238, 1)', + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'rgba(81, 203, 238, 1)', + }, +}), 'input') + +const ErrorMessage = createComponent(() => ({ + color: 'red', +}), Text) + +const renderLabel = (label) => { + return +} + +const renderField = (name, hasError) => { + return ( + ( + + )} + /> + ) +} + +const renderError = (name, hasError, errors) => { + if (!hasError) { + return null + } + return ( + + {getIn(errors, name)} + + ) +} + +const FormRow = ({name, label, errors, touched}:Props) => { + const hasError = getIn(touched, name) && getIn(errors, name) + return ( + + {renderLabel(label)} + {renderField(name, hasError)} + {renderError(name, hasError, errors)} + + ) +} + +export default FormRow diff --git a/src/main/forms/components/FormsPage.react.js b/src/main/forms/components/FormsPage.react.js new file mode 100644 index 0000000..782965c --- /dev/null +++ b/src/main/forms/components/FormsPage.react.js @@ -0,0 +1,57 @@ +// @flow +import React from 'react' +import {Box} from 'indoqa-react-fela' +import {Link} from 'react-router' +import {createComponent} from 'react-fela' + +import MainMenuTemplate from '../../commons/components/templates/MainMenuTemplate.react' +import ButtonLink from '../../commons/components/atoms/ButtonLink.react' + +import type {User} from '../types/User' + +type Props = { + users: { [string]:User }, +} + +const TableData = createComponent(({theme}) => ({ + padding: theme.spacing.space1, +}), 'td') + +const renderUserRow = (user: User) => { + return ( + + {user.name} + {user.email} + + + Edit + + + + ) +} + +class FormsPage extends React.Component { + + render() { + const {users} = this.props + return ( + + + + + {Object.keys(users).map((k) => renderUserRow(users[k]))} + +
+ + + Add user + + +
+
+ ) + } +} + +export default FormsPage diff --git a/src/main/forms/components/FormsPage.redux.js b/src/main/forms/components/FormsPage.redux.js new file mode 100644 index 0000000..9ba9e71 --- /dev/null +++ b/src/main/forms/components/FormsPage.redux.js @@ -0,0 +1,12 @@ +// @flow +import {connect} from 'react-redux' +import {selectUsers} from '../store/forms.selectors' + +import type {FormsState} from '../types/FormsState' +import FormsPage from './FormsPage.react' + +const mapStateToProps = (state: FormsState) => ({ + users: selectUsers(state), +}) + +export default connect(mapStateToProps)(FormsPage) diff --git a/src/main/forms/components/UserForm.react.js b/src/main/forms/components/UserForm.react.js new file mode 100644 index 0000000..f299326 --- /dev/null +++ b/src/main/forms/components/UserForm.react.js @@ -0,0 +1,58 @@ +// @flow +import * as React from 'react' +import {Form, Formik} from 'formik' +import {Box} from 'indoqa-react-fela' +import yup from 'yup' +import {Link} from 'react-router' + +import FormRow from './FormRow.react' +import AddressesForm from './AddressesForm.react' +import ButtonLink from '../../commons/components/atoms/ButtonLink.react' + +import type {User} from '../types/User' + +type Props = { + user: User, + onSubmit: Function, +} + +const validationSchema = () => { + return yup.object().shape({ + name: yup.string().required(), + email: yup.string().required('E-mail is required'), + addresses: yup.array().of(yup.object().shape({ + country: yup.string().required('Country is required'), + zipCode: yup.string().required('ZIP code is required'), + })), + }) +} + +const UserForm = ({user, onSubmit}:Props) => { + return ( + onSubmit(values, setErrors)} + initialValues={user} + validateOnChange={false} + validationSchema={validationSchema} + render={({values, errors, touched}) => { + return ( +
+ + + + + + Cancel + + + + + ) + }} + /> + ) +} + +export default UserForm diff --git a/src/main/forms/components/UserPage.react.js b/src/main/forms/components/UserPage.react.js new file mode 100644 index 0000000..1776dbe --- /dev/null +++ b/src/main/forms/components/UserPage.react.js @@ -0,0 +1,56 @@ +// @flow +import React from 'react' +import {Box} from 'indoqa-react-fela' +import {withRouter, type Router} from 'react-router' + +import MainMenuTemplate from '../../commons/components/templates/MainMenuTemplate.react' +import UserForm from './UserForm.react' + +import type {User} from '../types/User' + +type Props = { + router: Router, + currentUser: User, + loadUser: Function, + postUser: Function, +} + +class UserPage extends React.Component { + + constructor(props: Props) { + super(props) + this.postUser = this.postUser.bind(this) + } + + componentWillMount() { + const {router, loadUser} = this.props + const {id} = router.params + loadUser(id) + } + + postUser: (User) => void + + postUser(user: User, setErrors) { + const {postUser} = this.props + postUser(user, setErrors) + } + + render() { + const {currentUser} = this.props + + if (currentUser === null) { + return null + } + + const operation = currentUser.id === '' ? 'Add' : 'Edit' + return ( + + + + + + ) + } +} + +export default withRouter(UserPage) diff --git a/src/main/forms/components/UserPage.redux.js b/src/main/forms/components/UserPage.redux.js new file mode 100644 index 0000000..3c1e8f7 --- /dev/null +++ b/src/main/forms/components/UserPage.redux.js @@ -0,0 +1,14 @@ +// @flow +import {connect} from 'react-redux' +import {selectCurrentUser} from '../store/forms.selectors' + +import {loadUser, postUser} from '../store/forms.actions' +import UserPage from './UserPage.react' + +import type {FormsState} from '../types/FormsState' + +const mapStateToProps = (state: FormsState) => ({ + currentUser: selectCurrentUser(state), +}) + +export default connect(mapStateToProps, {loadUser, postUser})(UserPage) diff --git a/src/main/forms/store/forms.actions.js b/src/main/forms/store/forms.actions.js new file mode 100644 index 0000000..740e56a --- /dev/null +++ b/src/main/forms/store/forms.actions.js @@ -0,0 +1,26 @@ +// @flow +import type {Action} from '../types/FormsActions' + +import type {User} from '../types/User' + +export const loadUser = (id: string): Action => ({ + type: 'FORMS_LOAD_USER', + id, +}) + +export const saveUser = (user: User): Action => ({ + type: 'FORMS_SAVE_USER', + user, +}) + +export const postUser = (user: User, setErrors: Function): Action => ({ + type: 'FORMS_POST_USER', + user, + setErrors, +}) + + +export const setCurrentUser = (currentUser: User): Action => ({ + type: 'FORMS_SET_CURRENT_USER', + currentUser, +}) diff --git a/src/main/forms/store/forms.epics.js b/src/main/forms/store/forms.epics.js new file mode 100644 index 0000000..5a1da9e --- /dev/null +++ b/src/main/forms/store/forms.epics.js @@ -0,0 +1,52 @@ +import {Observable} from 'rxjs/Observable' +import {push} from 'react-router-redux' +import shortid from 'shortid' + +import {selectUsers} from './forms.selectors' +import {setCurrentUser, saveUser} from './forms.actions' +import {createNewUser} from './forms.factory' + +const selectUser = (id, state) => { + if (id === undefined) { + return createNewUser() + } + + const user = selectUsers(state)[id] + if (user === undefined) { + throw Error(`User with id '${id}' does not exist.`) + } + return user +} + +const postUser = (user) => { + if (user.id === '') { + user.id = shortid.generate() + } + return saveUser(user) +} + +const loadCurrentUserEpic$ = (action$, store) => + action$ + .ofType('FORMS_LOAD_USER') + .map((action) => setCurrentUser(selectUser(action.id, store.getState()))) + +const postUserEpic$ = (action$) => + action$ + .ofType('FORMS_POST_USER') + .mergeMap((action) => { + // Formik does not allow setting initial errors, hence this work-around is necessary to pass the setErrors method of the form + // see https://github.com/jaredpalmer/formik/issues/288 + const {user, setErrors} = action + if (user.name.startsWith('G')) { + setErrors({name: 'Names starting with \'G\' are not allowed.'}) + // do not emit an action observable in the case of an error + return Observable.of().ignoreElements() + } + + return Observable.merge( + Observable.of(postUser(action.user)), + Observable.of(push('/forms')) + ) + }) + +export default [loadCurrentUserEpic$, postUserEpic$] diff --git a/src/main/forms/store/forms.factory.js b/src/main/forms/store/forms.factory.js new file mode 100644 index 0000000..0b253e1 --- /dev/null +++ b/src/main/forms/store/forms.factory.js @@ -0,0 +1,25 @@ +// @flow +import type {User} from '../types/User' +import type {Address} from '../types/Address' + +const createNewUser = ():User => { + return { + id: '', + name: '', + email: '', + addresses: [], + lastModified: new Date(), + } +} + +const createNewAddress = ():Address => ({ + street: '', + city: '', + zipCode: '', + country: '', +}) + +export { + createNewUser, + createNewAddress, +} diff --git a/src/main/forms/store/forms.reducer.js b/src/main/forms/store/forms.reducer.js new file mode 100644 index 0000000..a698c68 --- /dev/null +++ b/src/main/forms/store/forms.reducer.js @@ -0,0 +1,75 @@ +// @flow +import type {FormsState} from '../types/FormsState' +import type {User} from '../types/User' +import type {Action} from '../types/FormsActions' + +const user1: User = { + id: 'HyJifGwFG', + name: 'Maier', + email: 'w.maier@example.com', + addresses: [ + { + street: 'Schottenring 3', + city: 'Vienna', + zipCode: '1010', + country: 'Austria', + }, + { + street: 'Heinrichstrasse 7', + city: 'Graz', + zipCode: '8010', + country: 'Austria', + } + ], + lastModified: new Date(), +} + +const user2: User = { + id: 'r1rozfvFf', + name: 'Gruber', + email: 'f.gruber@example.com', + addresses: [], + lastModified: new Date(), +} + +const initialState: FormsState = { + users: { + [user1.id]: user1, + [user2.id]: user2, + }, + currentUser: null, +} + +export default (state: FormsState = initialState, action: Action) => { + switch (action.type) { + case 'FORMS_SAVE_USER': + return { + ...state, + users: { + ...state.users, + [action.user.id]: { + ...action.user, + addresses: [ + ...action.user.addresses, + ], + lastModified: new Date(), + }, + }, + } + + case 'FORMS_LOAD_USER': + return { + ...state, + currentUser: null, + } + + case 'FORMS_SET_CURRENT_USER': + return { + ...state, + currentUser: action.currentUser, + } + + default: + return state + } +} diff --git a/src/main/forms/store/forms.selectors.js b/src/main/forms/store/forms.selectors.js new file mode 100644 index 0000000..17b75e9 --- /dev/null +++ b/src/main/forms/store/forms.selectors.js @@ -0,0 +1,8 @@ +// @flow +import {createSelector} from 'reselect' + +import type {FormsState} from '../types/FormsState' +import {selectFormsState} from '../../app/selectors.js' + +export const selectUsers = createSelector(selectFormsState, (state: FormsState) => state.users) +export const selectCurrentUser = createSelector(selectFormsState, (state: FormsState) => state.currentUser) diff --git a/src/main/forms/types/Address.js b/src/main/forms/types/Address.js new file mode 100644 index 0000000..24c3d8b --- /dev/null +++ b/src/main/forms/types/Address.js @@ -0,0 +1,7 @@ +// @flow +export type Address = { + street: string, + city: string, + zipCode: string, + country: string, +} diff --git a/src/main/forms/types/FormsActions.js b/src/main/forms/types/FormsActions.js new file mode 100644 index 0000000..03334db --- /dev/null +++ b/src/main/forms/types/FormsActions.js @@ -0,0 +1,28 @@ +// @flow +import type {User} from './User' + +type SaveUserAction = { + type: 'FORMS_SAVE_USER', + user: User, +} + +type PostUserAction = { + type: 'FORMS_POST_USER', + user: User, +} + +type LoadUserAction = { + type: 'FORMS_LOAD_USER', + id: string, +} + +type SetCurrentUserAction = { + type: 'FORMS_SET_CURRENT_USER', + currentUser: User, +} + +export type Action = + | SaveUserAction + | PostUserAction + | LoadUserAction + | SetCurrentUserAction diff --git a/src/main/forms/types/FormsState.js b/src/main/forms/types/FormsState.js new file mode 100644 index 0000000..8526086 --- /dev/null +++ b/src/main/forms/types/FormsState.js @@ -0,0 +1,7 @@ +// @flow +import type {User} from './User' + +export type FormsState = { + +users: { [string]: User }, + +currentUser: ?User, +} diff --git a/src/main/forms/types/User.js b/src/main/forms/types/User.js new file mode 100644 index 0000000..5ffd46b --- /dev/null +++ b/src/main/forms/types/User.js @@ -0,0 +1,10 @@ +// @flow +import type {Address} from './Address' + +export type User = { + id: string, + name: string, + email: string, + addresses: Array
, + lastModified: Date, +} diff --git a/src/main/time/components/TimePage.react.js b/src/main/time/components/TimePage.react.js index 501a057..1e07774 100644 --- a/src/main/time/components/TimePage.react.js +++ b/src/main/time/components/TimePage.react.js @@ -13,7 +13,7 @@ class TimePage extends React.Component { render() { return ( - + diff --git a/src/main/todos/components/AddTodo.react.js b/src/main/todos/components/AddTodo.react.js index 9c19b37..3ef3ab1 100644 --- a/src/main/todos/components/AddTodo.react.js +++ b/src/main/todos/components/AddTodo.react.js @@ -1,5 +1,6 @@ // @flow import React from 'react' +import {Box} from 'indoqa-react-fela' type Props = { addTodo: Function, @@ -9,7 +10,7 @@ const AddTodo = ({addTodo}: Props) => { let input return ( -
+
{ e.preventDefault() @@ -29,7 +30,7 @@ const AddTodo = ({addTodo}: Props) => { Add Todo
-
+
) } diff --git a/src/main/todos/components/Footer.react.js b/src/main/todos/components/Header.react.js similarity index 88% rename from src/main/todos/components/Footer.react.js rename to src/main/todos/components/Header.react.js index e6abe59..a6e6ba3 100644 --- a/src/main/todos/components/Footer.react.js +++ b/src/main/todos/components/Header.react.js @@ -2,7 +2,7 @@ import React from 'react' import FilterLink from './FilterLink.redux' -const Footer = () => ( +const Header = () => (

Show: {' '} @@ -20,4 +20,4 @@ const Footer = () => (

) -export default Footer +export default Header diff --git a/src/main/todos/components/TodosPage.react.js b/src/main/todos/components/TodosPage.react.js index 5bba35d..c56cd8d 100644 --- a/src/main/todos/components/TodosPage.react.js +++ b/src/main/todos/components/TodosPage.react.js @@ -4,7 +4,7 @@ import {Box} from 'indoqa-react-fela' import MainMenuTemplate from '../../commons/components/templates/MainMenuTemplate.react' import AddTodo from '../components/AddTodo.redux' -import Footer from '../components/Footer.react' +import Header from './Header.react' import TodoList from '../components/TodoList.redux' class TodosPage extends React.Component<{}> { @@ -12,10 +12,10 @@ class TodosPage extends React.Component<{}> { render() { return ( - + +
-