MithrilのTodo ListをはじめからていねいにTypescriptで(2)
前回の続きです。
1. 完了・未完了を表すcompletedによってスタイルを変える
テスト用に completed = trueとしている。
src/models/TodoState.ts
export default class TodoState {
constructor(
public id: number,
public text: string,
public completed: boolean = true // TODO: true -> false
) {}
}
src/components/Todo.tsx
import { ClassComponent, Vnode } from 'mithril';
import * as m from 'mithril';
interface IAttr {
text: string;
completed: boolean ;
}
export default class Todo implements ClassComponent<IAttr> {
public view(vnode: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
const { text, completed } = vnode.attrs;
return (
<li style = {{textDecoration: completed ? 'line-through' : 'none'}}>
{text}
</li>);
}
}
actionCreatorからcompleted要素を操作する
src/actions/index.ts
import { Action } from 'redux';
import { createAction } from 'redux-actions';
export const ADD = 'ADD_TODO';
export const TOGGLE = 'TOGGLE_TODO';
export interface IAddTodoAction extends Action {
type: 'ADD_TODO';
payload: {
id: number;
text: string;
};
}
export interface IToggleTodoAction extends Action {
type: 'TOGGLE_TODO';
payload: {
id: number;
};
}
let nextTodoId = 0;
export const addTodo = createAction(ADD, (text: string) => ({ text, id: nextTodoId++ }));
export const toggleTodo = createAction(TOGGLE, (id: number) => ({ id }));
src/reducers/todos.ts
import { handleActions } from 'redux-actions';
import { ADD, IAddTodoAction, IToggleTodoAction, TOGGLE } from '../actions';
import TodoState from '../models/TodoState';
export default handleActions({
[ADD]: (state: TodoState[], { payload }: IAddTodoAction) => {
return [...state, new TodoState(payload.id, payload.text)];
},
[TOGGLE]: (state: TodoState[], { payload }: IToggleTodoAction) => {
const { id } = payload;
return state.map((t) => {
// actionCreatorに渡したidと一致するtodoのみ処理
if (t.id !== id) {
return t;
}
// completedだけを反転
return new TodoState(t.id, t.text, !t.completed);
});
},
}, []);
確認
src/app.ts
import * as m from 'mithril';
import App from './components/App';
import {createStore } from 'redux';
import { addTodo, toggleTodo } from './actions'
import reducers from './reducers';
import Provider from './mithril-redux';
const store = createStore(reducers);
store.dispatch(addTodo('Hello World!'));
store.dispatch(toggleTodo(0));
const root = document.getElementById('app');
function render(){
m.render(root, m(Provider,{ store }, m(App)));
}
render();
store.subscribe(render);
2. クリックしてcompletedの値を変える
src/containers/VisibleTodoList.tsx
import TodoList from '../components/TodoList';
import { connect } from '../mithril-redux';
import TodoState from '../models/TodoState';
import { toggleTodo } from '../actions';
interface IStateToProps { todos: TodoState[]; }
interface IDispatchToProps{ onTodoClick: Function; }
const mapStateToProps = (store): IStateToProps => {
return { todos: store.todos };
};
const mapDispatchToProps = (dispatch:Function):IDispatchToProps => {
return {
onTodoClick: (id:number) => {
dispatch(toggleTodo(id))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
src/components/TodoList.tsx
import * as m from 'mithril';
import { ClassComponent, Vnode } from 'mithril';
import TodoState from '../models/TodoState';
import Todo from './Todo';
interface IAttr {
props: {
todos: TodoState[];
onTodoClick: (id: number) => void;
};
}
export default class TodoList implements ClassComponent<IAttr> {
public view({ attrs:{ props } }: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
const { todos, onTodoClick } = props;
return (
<ul>
{todos.map(todo => <Todo {...todo} onClick={() => {onTodoClick(todo.id);}} />)}
</ul>);
}
}
src/components/Todo.tsx
import { ClassComponent, Vnode } from 'mithril';
import * as m from 'mithril';
interface IAttr {
text: string;
completed: boolean;
onClick: (id: number) => void;
}
export default class Todo implements ClassComponent<IAttr> {
public view({ attrs }: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
const { text, completed, onClick } = attrs;
return (
<li
onclick={onClick}
style = {{ textDecoration: completed ? 'line-through' : 'none' }}>
{text}
</li>);
}
}
3. actionCreatorとreducerでフィルターの値をstore(state)に格納
actionCreatorの作成
src/actions/filter.ts
import { Action } from 'redux';
import { createAction } from 'redux-actions';
export const SET_VISIBILITY = 'SET_VISIBILITY_FILTER';
export const ALL = 'SHOW_ALL';
export const COMPLETED = 'SHOW_COMPLETED';
export const ACTIVE = 'SHOW_ACTIVE';
export type VisibilityFilterType = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';
export interface IVisibilityFilter extends Action {
type: 'SET_VISIBILITY_FILTER';
payload: {
filter: VisibilityFilterType;
};
}
export const setVisibilityFilter = createAction(SET_VISIBILITY, (filter: VisibilityFilterType) => ({ filter }));
src/reducers/visibilityFilter.ts
import { handleActions } from 'redux-actions';
import { ALL, IVisibilityFilter, SET_VISIBILITY } from '../actions/filter';
export default handleActions({
[SET_VISIBILITY]: (state, { payload:{ filter } }: IVisibilityFilter) => {
return filter;
},
}, ALL);
src/reducers/index.ts
import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';
export default combineReducers({
todos, visibilityFilter,
});
確認
src/app.ts
import * as m from 'mithril';
import App from './components/App';
import {createStore } from 'redux';
import { addTodo, toggleTodo } from './actions'
import { setVisibilityFilter, COMPLETED } from './actions/filter'
import reducers from './reducers';
import Provider from './mithril-redux';
const store = createStore(reducers);
store.dispatch(addTodo('Hello World!'));
const root = document.getElementById('app');
function render(){
m.render(root, m(Provider,{ store }, m(App)));
}
render();
store.subscribe(render);
console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_ALL"}
store.dispatch(setVisibilityFilter(COMPLETED))
console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_COMPLETED"}
4. フィルターの値によってviewを変更(手動でフィルターを操作して動作確認)
import { toggleTodo } from '../actions';
import { VisibilityFilterType } from '../actions/filter';
import TodoList from '../components/TodoList';
import { connect } from '../mithril-redux';
import TodoState from '../models/TodoState';
interface IStateToProps {
todos: TodoState[];
}
interface IDispatchToProps { onTodoClick: (id: number) => void;}
const getVisibleTodos = (todos: TodoState[], filter:VisibilityFilterType):TodoState[] => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter((t) => t.completed)
case 'SHOW_ACTIVE':
return todos.filter((t) => !t.completed)
}
}
const mapStateToProps = (store): IStateToProps => {
return { todos: getVisibleTodos(store.todos, store.visibilityFilter) };
};
const mapDispatchToProps = (dispatch): IDispatchToProps => {
return {
onTodoClick: (id: number) => {
dispatch(toggleTodo(id));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
5. リンクをクリックしてフィルターを操作してviewを変更
とりあえずリンク
src/components/Link.tsx
import * as m from 'mithril';
import { ClassComponent, Vnode } from 'mithril'; // tslint:disable-line: no-duplicate-imports
interface IAttr {}
export default class Link implements ClassComponent<IAttr> {
view({children}: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
return (<a href="#">{children}</a>);
}
}
src/components/Footer.tsx
import * as m from 'mithril';
import { ClassComponent, Vnode } from 'mithril'; // tslint:disable-line: no-duplicate-imports
import Link from './Link';
interface IAttr {}
export default class Footer implements ClassComponent<IAttr> {
view({children}: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
return (
<p>
Show:
{" "}
<Link>
All
</Link>
{", "}
<Link>
Active
</Link>
{", "}
<Link>
Completed
</Link>
</p>
);
}
}
src/components/App.tsx
import * as m from 'mithril';
import { ClassComponent, Vnode } from 'mithril'; // tslint:disable-line: no-duplicate-imports
import AddTodo from '../containers/AddTodo';
import VisibleTodoList from '../containers/VisibleTodoList';
import Footer from './Footer';
interface IAttr {}
export default class App implements ClassComponent<IAttr> {
view(vnode: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
return (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>);
}
}
Linkコンテナでdispatch(setVisibilityFilter())を呼び出せるようにする
import { connect } from 'react-redux';
import { setVisibilityFilter } from '../actions';
import Link from '../components/Link';
import { VisibilityFilterType } from '../states/VisibilityFilterType';
interface IState{
visibilityFilter: VisibilityFilterType
}
interface IProps{
filter:VisibilityFilterType
}
interface IStateToProps{
state:any
}
interface IDispatchToProps{
onClick: Function
}
const mapStateToProps = (state:IState, ownProps:IProps):IStateToProps => {
return {
state:state
}
}
const mapDispatchToProps = (dispatch, ownProps:IProps):IDispatchToProps => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link);
Linkコンテナでdispatchを呼べるようにする。
src/containers/FilterLink.ts
import { setVisibilityFilter, VisibilityFilterType } from '../actions/filter';
import Link from '../components/Link';
import { connect } from '../mithril-redux';
interface IOwnProps {
filter: VisibilityFilterType;
}
interface IDispatchToProps {
onClick: ()=>void;
}
const mapDispatchToProps = (dispatch, ownProps: IOwnProps): IDispatchToProps => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter));
},
};
};
export default connect(
null,
mapDispatchToProps,
)(Link);
src/components/Footer.tsx
import * as m from 'mithril';
import { ClassComponent, Vnode } from 'mithril'; // tslint:disable-line: no-duplicate-imports
import { ACTIVE, ALL, COMPLETED } from '../actions/filter';
import FilterLink from '../containers/FilterLink';
interface IAttr {}
export default class Footer implements ClassComponent<IAttr> {
public view({ children }: Vnode<IAttr, this>): Vnode<IAttr, HTMLElement> {
return (
<p>
Show:
{' '}
<FilterLink filter={ALL}> {/* ここのfilter属性がcontainerのownProps.filter となる */}
All
</FilterLink>
{', '}
<FilterLink filter={ACTIVE}>
Active
</FilterLink>
{', '}
<FilterLink filter={COMPLETED}>
Completed
</FilterLink>
</p>
);
}
}
LinkコンポーネントでクリックしたときにonClickを呼ぶようにする。
src/components/Link.tsx
import * as m from 'mithril';
import { ClassComponent, Vnode } from 'mithril'; // tslint:disable-line: no-duplicate-imports
interface IAttr {
props:{
onClick: ()=>void
};
}
export default class Link implements ClassComponent<IAttr> {
public view({ children, attrs:{ props:{ onClick } } }: Vnode<IAttr, this>) {
return (
<a href="#" onclick={(e: Event) => {
e.preventDefault();
onClick();
}
}>
{children}
</a>);
}
}
activeな状態のリンクを押せないようにする
src/containers/FilterLink.ts
import { setVisibilityFilter, VisibilityFilterType } from '../actions/filter';
import Link from '../components/Link';
import { connect } from '../mithril-redux';
interface IOwnProps {
filter: VisibilityFilterType;
}
interface IDispatchToProps {
onClick: ()=>void;
}
const mapStateToProps = (state, ownProps:IOwnProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps: IOwnProps): IDispatchToProps => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter));
},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Link);