0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

MithrilのTodo ListをはじめからていねいにTypescriptで(2)

Posted at

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);

この時点のソース

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?