一构建开发环境
todolist的开发工具包为webpack+es6+react,首先需要构建开发环境,我在上一篇文章webpack构建ant-design中已经构建了ant-design的开发环境,这里也是用到这边文章中所用的环境,在这个实例中需要用到箭头函数,所以还需要安装一下 stage-0,安装语句如下:
npm install --save-dev babel-preset-stage-0
修改.babelrc文件如下
{
"presets": [
"es2015",
"react",
"stage-0"
],
"sourceMaps": true
}
二分析todolist结构
完成版的todolist如上图所示,按照react组件化编程的风格,我大致将todolist分为三个组件,分别是TodoHeader,用来输入任务名称,TodoMain,用来展示任务,TodoFooter,用来全选和清楚选中的任务,我创建的文件,及问价目录如下
三 输入,存储任务
index.js作为webpack打包的入口文件,App作为主要的组件来引用上面三个组件。
index.js
require("./style/index.less");
import ReactDom from 'react-dom';
import React from 'react';
import App from '../src/component/App';
ReactDom.render(<App/>, document.getElementById('root'));
App.jsimport React from 'react';import TodoHeader from './TodoHeader';
import TodoMain from './TodoMain';
import TodoFooter from './TodoFooter';
class App extends React.Component{
constructor(props){
super(props);
this.db = localStorage; //定义一个localStorage容易
let todos = this.db.getItem('todos') || []; //通过键todos,来获取localStorage容器中的值
this.state = {
todos: eval('(' + todos + ')'),
isAllChecked: false //用来表示是否全选
};
}
componentDidMount(){
// let todos = eval('(' + this.state.todos + ')');
// if(todos.length > 0){
// this.setState({todos: todos});
// }
}
allChecked() {
//全选函数,供子组件调用
let isAllChecked = false;
// every() 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)。
if(this.state.todos.every((todo) => todo.isDone)){
isAllChecked = true;
}
this.setState({
todos: this.state.todos,
isAllChecked: isAllChecked
})
}
clearDone = () =>{
//清除选中的项目
let todos = this.state.todos.filter(todo => !todo.isDone); //过滤没有选中的项目
this.setState({
todos: todos,
isAllChecked: false,
});
this.db.setItem('todos', JSON.stringify(todos)); //修改localStorage中键todos的值,以json数组的形式存储,方便后续取出操作
}
addTodo =(todoItem) =>{
//添加项目操作
this.state.todos.push(todoItem);//todo列表 ,向todos数组中插入一条记录
this.db.setItem('todos', JSON.stringify(this.state.todos)); //将所有的记录再次保存如localStorage中
this.allChecked();
}
deleteTodo = (index) =>{
//删除一个项目
this.state.todos.splice(index, 1);//通过传过来的索引值,删除数组中的一条记录
this.setState({todos: this.state.todos}); //
this.db.setItem('todos', JSON.stringify(this.state.todos));//将更新的todos数组,插入到localStorage中
}
changeTodoState = (index, isDone, isChangeAll = false) => {
// 改变任务状态,传递给TodoItem和Footer组件的方法
if(isChangeAll){
this.setState({
todos: this.state.todos.map( (todo) =>{
todo.isDone = isDone;
return todo;
}),isAllChecked: isDone
})
}else{
this.state.todos[index].isDone = isDone;
this.allChecked();
}
this.db.setItem('todos', JSON.stringify(this.state.todos));
}
render(){
let info = {
isAllChecked: this.state.isAllChecked,
todoCount: this.state.todos.length || 0,
todoDoneCount: (this.state.todos && this.state.todos.filter((todo) => todo.isDone)).length || 0
};
return(
<div className="todo-wrap">
<TodoHeader addTodo={this.addTodo} name="add" />
<TodoMain todos={this.state.todos} deleteTodo={this.deleteTodo} changeTodoState={this.changeTodoState} {...info}/>
<TodoFooter {...info} changeTodoState={this.changeTodoState} clearDone={this.clearDone} />
</div>
);
}
}
export default App;
在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同。
三组件TodoHeader
在App.js中引用了组件TodoHeader,并给这个子组件传入了方法addTodo ,组件TodoHeader的代码如下,用户在输入框中输入,点击回车按钮,保存用户的输入到localStorage中去。
import React from 'react';
class TodoHeader extends React.Component{
constructor(props){
super(props);
this.state = {
};
this.handlerKeyUp = this.handlerKeyUp.bind(this); //绑定键盘事件,也可以使用箭头函数
}
componentDidMount(){
}
handlerKeyUp(e){
//鼠标回车事件
if(e.keyCode == 13){
//键盘敲击回车时间
let value = e.target.value; //获取输入框中输入的值
if(!value) return false; //如果输入框没有输入,则返回false
let newTodoItem = {
text: value,
isDone: false
};//将输入的值保存在一个对象中
e.target.value = '';//将输入框的值置空
this.props.addTodo(newTodoItem) //调用父组件的方法,通过this.props调用父控件的方法,将输入的值传递给父控件,在父控件中对state的值进行修改,从而重新渲染页面。
}
}
render() {
return(
<div className="todo-header">
<h1 className="text-center">React Todo</h1>
<div className="form-horizontal">
<div className="form-group">
<label className="col-md-2 control-label">Task</label>
<div className="col-md-10">
<input onKeyUp={this.handlerKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认" className="form-control"/>
</div>
</div>
</div>
</div>
)
}
};
export default TodoHeader;
四 展示组件TodoMain
定义一个组件TodoItem,来处理具体某一条的记录,然后再TodoMain中来处理出所有的数据
TodoItem.js代码如下
import React from 'react';
export default class TodoItem extends React.Component{
constructor(props){
super(props);
this.state = {
isShow: false,
};
}
componentDidMount(){
}
handlerChange =()=>{
//多选框处理事件,选中则表示已处理
let isDone = !this.props.isDone;
this.props.changeTodoState(this.props.index, isDone);
}
handlerDelete =()=>{
//删除事件
this.props.deleteTodo(this.props.index);
}
handlerMouseOver =()=>{
//鼠标移入事件
this.setState({isShow:true});
}
handlerMouseOut =() =>{
//鼠标移出事件
this.setState({isShow:false});
}
render(){
let className = this.props.isDone? 'task-done': '';
return(
<li className={this.props.isDone?"list-group-item-success item":"item"} onMouseOver={this.handlerMouseOver} onMouseOut={this.handlerMouseOut}>
<label>
<input type="checkbox" checked={this.props.isDone} onChange={this.handlerChange} className="pull-left"/>
<span className={className}>{this.props.text}</span>
</label>
<div className="pull-right">
<button type="button" className={ this.state.isShow?"btn btn-xs":"btn btn-xs close"} onClick={this.handlerDelete}>删除</button>
</div>
</li>
)
}
}
TodoMain.js代码如下
import React from 'react';
import TodoItem from './TodoItem';
export default class TodoMain extends React.Component{
constructor(props){
super(props);
this.state = {
};
}
componentDidMount(){
}
render() {
if(this.props.todos.length == 0){
return(
<div className="todo-empty">恭喜您,目前没有待办事项!</div>
)
}else{
return (
<ul className="todo-main">
{
this.props.todos.map((todo, index) => {
return <TodoItem text={todo.text} isDone={todo.isDone} index = {index} {...this.props} key={index} />
})
}
<li className="item">
<label>
<span><strong>{this.props.todoCount}</strong>总数/<strong>{this.props.todoDoneCount}</strong>已完成</span>
</label>
</li>
</ul>
)
}
}
}
{..this.props}将App中的方法,通过props传递给组件TodoIten中
五 TodoFooter
TodoFooter代码如下
import React from 'react';
export default class TodoFooter extends React.Component{
constructor(props){
super(props);
this.state = {
};
}
componentDidMount(){
}
handlerSelectAll =(e) =>{
//全选事件,调用父组件的全选事件
this.props.changeTodoState(null, e.target.checked, true);
}
handlerDeleteDone = () => {
//清除已选任务操作,调用父组件的事件
this.props.clearDone();
}
render(){
let count = this.props.todoCount;
if(count > 0){
return(
<div className="todo-footer">
<label>
<input type="checkbox" checked={this.props.isAllChecked} onChange={this.handlerSelectAll} />全选
</label>
<div className="clearTask">
<button className="btn" onClick={this.handlerDeleteDone}>清除已完成任务</button>
</div>
</div>
)
}else{
return (
<div className="todo-footer"></div>
)
}
}
}
六项目中用到的样式
样式文件放在style/index.less文件中
body {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
line-height: 1.42857143;
color: #333;
background-color: #fff;
}
#root{
width: 1170px;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.todo-header{
.form-horizontal .form-group {
margin-right: -15px;
margin-left: -15px;
}
.form-group {
margin-bottom: 15px;
}
.form-horizontal .control-label {
padding-top: 7px;
margin-bottom: 0;
text-align: right;
}
.col-md-2 {
float: left;
width: 16.66666667%;
}
.col-md-10{
width: 83.33333333%;
}
.form-control {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
}
.todo-main{
padding-left: 0;
margin-bottom: 20px;
.item{
position: relative;
display: block;
padding: 10px 15px;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid #ddd;
.pull-left{
float: left!important;
}
}
.list-group-item-success{
color: #3c763d;
background-color: #dff0d8;
}
}
.todo-main:first-child{
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.todo-footer{
margin-bottom: 40px;
.clearTask{
display: inline-block;
margin-left: 20px;
.btn{
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
}
.btn:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
}
}
.todo-empty{
}
input[type=checkbox], input[type=radio] {
margin: 4px 0 0;
margin-top: 1px\9;
line-height: normal;
}
input[type="checkbox"] {
margin-right: 10px;
}
button.close {
display: none;
font-size: 12px;
height: 18px;
width: 18px;
}
button.close {
-webkit-appearance: none;
padding: 0;
cursor: pointer;
background: 0 0;
border: 0;
}
.close {
float: right;
font-size: 21px;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: .2;
}
.btn-group-xs>.btn, .btn-xs {
padding: 1px 5px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
.todo-wrap{
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
margin-left: 25%;
}
.todo-empty{
text-align: center;
}
.text-center {
text-align: center;
}
.h1, h1 {
font-size: 36px;
}