基于dva-cli实现todoList

功能点

  • 输入框输入内容回车, 增加一条记录, 同时输入框内容被清空
  • 双击每一条记录的文字内容, 进行编辑修改, 回车或者点击其他区域完成修改
  • 点击每一条记录前面的圆圈, 切换该记录的完成或已完成状态, 圆圈和后面的文字内容随着状态的切换均有样式变化
  • 每一条记录均有删除图标, 点击完成该条记录的删除操作
  • 动态显示未完成事项的个数
  • 三个按钮All, Active, Completed可完成对当前所有事项, 未完成事项, 已完成事项的查询显示
  • 可一次性清除所有已完成事项
  • 所有数据的永久性存储, 即刷新页面或再次启动项目数据仍然保持上次操作的数据记录

使用antd的Input+Form控件实现输入框UI, 利用getFieldDecorator()将Input和表单进行双向绑定,每次用户向输入框输入内容并回车后,触发回调事件,通过getFieldsValue()拿到输入框的内容,并将该内容连同iscompletedid属性作为一个对象推入数组list中,发送dispatch请求完成list的更新。

通过自定义不同的页面路由,但使用同一个页面模板,完成全部事项、未完成事项和已完成事项的筛选显示。

在组件中import pathToRegexp from 'path-to-regexp',并传入location对象,通过location.pathname判断当前页面的路由。

对数组list使用filter()方法,根据当前的路由,过滤出所有iscompletedtruefalse的项。

每一条事项的展示放在component展示组件中,即对数组进行map()遍历操作,返回一个子组件,对这个子组件传入这个数组数组的每一项,以及每一项的索引,作为每一条事项的展示组件完成每一条事项的展示。

let todos = []
  if (list.length) {
    let showList = list.filter((value) => {
      //根据当前路由, 确定list数组中需要展示的项
      switch (pathname){
        case '/active': return value.iscompleted === false
        case '/completed': return value.iscompleted === true
        default: return true
      }
    })

    todos = showList.map(function(item, index) {
      //子组件用来定义每一项的展示, 传入展示项以及索引, 和每一个react组件所必须的唯一key值
      return <TodoItem todo={item} data_key={index} key={index+1}/>
    })
  }

在点击事件中携带该项纪录的索引值data_key发送dispatch请求,(react组件的唯一key值无法传入组件内部),利用数组的splice()方法(改变原数组)完成数组项的删除操作.

    deleteItem(state, action) {
      state.list.splice(action.payload.data_key, 1)

      localStorage.setItem('list',JSON.stringify(state.list))

      return {
        ...state
      }
    }

容器组件的声明采用stateless写法, 展示组件的声明采用ES6写法class TodoItem extends Component{}.

编辑的时候涉及两个状态的变化: isEditing是否编辑以及editText编辑框input的内容。双击文字内容,改变isEditing为true,使隐藏的input框便显示出来。

const ESCAPE_KEY = 27;
const ENTER_KEY = 13;

class TodoItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isEditing: false,
      editText: this.props.todo.item
    }
  }

  changeEditState() {
    this.setState({
      isEditing: true
    })
  }

  handleChange(e) {
    console.log(e.target.value)
    this.setState({
      editText: e.target.value
    });

  }

  handleSubmit(e) {
    let val = this.state.editText.trim();
    if (val) {
      // dispatch save
      console.log(val)
      this.setState({
        isEditing: false
      })
    }
  }


  handleKeyDown(event) {
    if (event.which === ESCAPE_KEY) {
      this.setState({
        editText: this.props.todo.item,
        isEditing: false
      });
    } else if (event.which === ENTER_KEY) {
      this.handleSubmit(event);
    }

    this.props.dispatch({
      type: `toDos/applyEdit`,
      payload: {
        editText: this.state.editText,
        id: this.props.todo.id
      }
    })
  }

  componentDidUpdate(prevProps, prevState) {
    //处理光标
    if (!prevState.isEditing && this.state.isEditing) {
      let node = ReactDom.findDOMNode(this.refs.editField);
      node.focus();
      console.log(node.value)
      node.setSelectionRange(node.value.length, node.value.length);
    }
  }

  render() {
    const {
      todo,
      dispatch,
      toDos
    } = this.props;
  }

    return (
        <div className={textClass} onDoubleClick={this.changeEditState.bind(this)} >{todo.item}</div>
        <input type="text" ref="editField" 
          className={editClass} 
          value={this.state.editText} 
          onBlur={this.handleSubmit.bind(this)}
          onKeyDown={this.handleKeyDown.bind(this)}
          onChange={this.handleChange.bind(this)}/>
        </div>
    )
}

数据的存储

localStorage是html5提供的一种本地存储的方法,可以把数据存储在本地浏览器,下次打开后仍然可以获取到存储的数据,如果在存储的数据量小的时候可以起到代替数据库的功能,比cookies更有优越性。特点:永久性存储.

Web Storage API --点这里
localStorage.setItem()方法用来创建新数据项和更新已存在的值。该方法接受两个参数——要创建/修改的数据项的键,和对应的值。
localStorage.getItem()可以从存储中获取一个数据项。该方法接受数据项的键作为参数,并返回数据值。

目前所有的浏览器中都会把localStorage的值类型限定为string类型,这个在对日常比较常见的JSON对象类型需要一些转换.

在对数据进行增删改操作之后,即数据list有所更改变化后,调用Storage.setItem()方法实时更改替换上次存储的数据。
localStorage.setItem('list',JSON.stringify(data))

在model文件的subscriptions中,每当进入页面路由,就从存储中取出所有数据。

      history.listen(({ pathname, query }) => {
        if (pathToRegexp(`/`).test(pathname) || pathToRegexp(`/completed`).test(pathname) || pathToRegexp(`/active`).test(pathname)) {
          let data = localStorage.getItem('list') || ''//取出数据
          dispatch({
            type: 'updateState',
            payload: {
              list: JSON.parse(data)//将数据装换成JSON对象,更新全局list
            }
          })
        }        
      }) 

总结

  1. 组件结构的设计
    容器组件和展示组件: 前者关注逻辑的处理和状态的变化, 后者设计成可复用的公共组件, 不涉及状态的处理, 只负责展示.
  //子组件
     let  todos = showList.map(function(item, index) {
        //每一条事项
        return <TodoItem todo={item} data_key={index} key={index+1}/>
    })

    let footer 
    if (list.length) {
      //操作按钮行
      footer = <Footer list={list} onDeleteAllCompleted={onDeleteAllCompleted}/>
    }
  1. 数据形式的设计
    将每一条事项作为一个对象,放入一个数组中。数组的每一项包括每一条事项的内容item, 是否完成iscompleted, 以及id标识。通过查找id完成每一项数据的更改操作。
      {
          item: item,
          iscompleted: false,
         // isEditing: false,
          id: id
        }
  1. react中样式的处理
  • CSS Modules的使用: 点击
import styles from './TodoItem.css'
//使用:
    <div className={styles.todos}></div>

CSS Modules提供了compose组合方法来处理样式复用

.correct{

}
.correct::before{
    position: relative;
    top: -10px;
}

.btnbasic {
    composes:correct; //compose组合
    float: left;
    border-radius: 50%;
    border: 1px solid #e4dada;
    width: 30px;
    height: 30px;
    margin: 10px 0;
    text-align: center;
    color: green;
    cursor: pointer;
}
  • classnames
    首先安装npm install classnames

If you are using css-modules, or a similar approach to abstract class "names" and the real className values that are actually output to the DOM, you may want to use the bind variant.

在CSS Modules中使用classnames需要绑定变量bind

   let classNames = require('classnames/bind')
   //用法:
    const itemClass = cx('item')

    const btnClass = cx({
      btnbasic: true,
      'iconfont icon-correct': iscompleted
    })

    <div className={itemClass}></div>
     <i className={btnClass}></i>

classnames介绍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容