TodoMVC
git clone https://github.com/tastejs/todomvc-app-template.git --depth 1
React的配置
- 可以使用脚手架
cnpm install -g create-react-app
create-react-app todos
# 支持es6,加上
cnpm install --save-dev babel-plugin-import react-app-rewired
- 这里我是自己配置的
下载项目所需要的包
# 初始化package.json
cnpm init -y
# 下载所需要的包
cnpm i webpack webpack-cli html-webpack-plugin webpack-dev-server babel-loader@7 babel-core babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime less less-loader css-loader style-loader babel-preset-react -D
cnpm i react react-dom -S
# 可能需要用到路由
cnpm i react-router-dom -S
# 可能需要用到配置文件
webpack.config.js
const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: path.join(__dirname, './src/main.js'),
output: {
path: path.join(__dirname, './dist'),
filename: 'bundle.js'
},
plugins: [ // 插件
new htmlWebpackPlugin({
template: path.join(__dirname, './src/index.html'),
filename: 'index.html'
})
],
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }, // 如果想要启用 CSS 模块化,可以为 css-loader 添加 modules 参数即可
{ test: /\.less$/, use: ['style-loader', 'css-loader?modules&localIdentName=[name]_[local]-[hash:5]', 'less-loader'] },
{ test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }
]
}
}
.babelrc
{
"presets": ["env", "stage-0","react"],
"plugins": ["transform-runtime"]
}
main.js
import React from 'react'
import ReactDom from 'react-dom'
import App from './App.jsx'
ReactDom.render(<App></App>,document.querySelector('#app'))
我们下载好的todomvc-app-template里面有个index.html,我们将其复制到我们的App.jsx
- 根据模板引入的文件,我们需要下载css包
cnpm i todomvc-common todomvc-app-css -S
- 由于React的注释是
{}
,所以我们需要把里面的html注释掉 class
在React是关键字,className才是JS的类名,需要替换掉;还有for要改为htmlFor
、autofocus改为autoFocus
,value改为defaultValue
,checked改为defaultChecked
…- App.jsx
import React from 'react'
import 'todomvc-common/base.css'
import 'todomvc-app-css/index.css'
export default class App extends React.Component{
constructor(props){
super(props)
}
render(){
return <div>
<section className="todoapp">
<header className="header">
<h1>todos</h1>
<input className="new-todo" placeholder="What needs to be done?" autoFocus={true}/>
</header>
<section className="main">
<input id="toggle-all" className="toggle-all" type="checkbox" />
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
<li className="completed">
<div className="view">
<input className="toggle" type="checkbox" />
<label>Taste JavaScript</label>
<button className="destroy"></button>
</div>
<input className="edit" defaultValue="Create a TodoMVC template" />
</li>
<li>
<div className="view">
<input className="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button className="destroy"></button>
</div>
<input className="edit" defaultValue="Rule the web" />
</li>
</ul>
</section>
<footer className="footer">
<span className="todo-count"><strong>0</strong> item left</span>
<ul className="filters">
<li>
<a className="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button className="clear-completed">Clear completed</button>
</footer>
</section>
<footer className="info">
<p>Double-click to edit a todo</p>
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</div>
}
}
启动项目(在package.json配置启动项)
"serve": "webpack-dev-server --open --port 3000 --hot"
cnpm run serve
以上对React配置比较熟悉可以不用看,或则使用脚手架;想要自己写模板也可以;直接跳过
实现业务功能
渲染数据
- 模拟一个数据,在main引入
let todos = [
{id:1,do:'learn vue',bool:false},
{id:2,do:'learn react',bool:false},
{id:3,do:'learn angular',bool:false},
]
// ReactDom.render(<App {...todos}></App>,document.querySelector('#app'))
ReactDom.render(<App data={todos}></App>,document.querySelector('#app'))
- 将数据保存在我们的App.jsx
constructor(props){
super(props)
// 1. 将数据储存在我们的state状态里面
this.state = {
todos:props.data
}
}
- 渲染数据
<ul className="todo-list">
{/* 1.1.循环我们的数据 */}
{this.state.todos.map((todo,index)=>{
return <li className=className={todo.bool?"completed":""} key={todo.id}>
<div className="view">
<input className="toggle" type="checkbox" onChange={()=>{this.changeCheckHandle(index)}} defaultChecked={todo.bool}/>
<label>{todo.do}</label>
<button className="destroy"></button>
</div>
<input className="edit" defaultValue={todo.do} />
</li>
})}
</ul>
- 当点击勾选按钮,(即
checkbox
改变时),那么数据重新改变
changeCheckHandle = (index)=>{
let todos = this.state.todos.map((item,i)=>{
if(i==index){
item.bool = !item.bool
}
return item
})
this.setState({
todos
})
}
- 我们可以查看一下我们的数据是否已经改变
双击编辑数据
- 我们从模板可知,完成状态是给每一个todo(
li
)添加一个类editing
,完成状态是completed
- 我们只需要在双击时给该条todo添加一个类
editing
即可进入编辑状态(在这里聚焦我试了好多方式总是有bug,后面我在试试后更) - 当使用
enter
编辑成功,使用esc
退出编辑
this.state = {
todos:props.data,
newTodo:{}//这是用来判断是否进入编辑状态
}
// return <li className={[todo.bool?"completed":"",this.state.newTodo==todo?"editing":""]} key={todo.id}>
return <li className={(todo.bool?"completed":"")+" "+(this.state.newTodo==todo?"editing":"")} key={todo.id}>
<div className="view">
<input className="toggle" type="checkbox" onChange={()=>{this.changeCheckHandle(index)}} defaultChecked={todo.bool}/>
<label onDoubleClick={()=>{this.editTodoHandle(todo)}}>{todo.do}</label>
<button className="destroy"></button>
</div>
<input
className="edit"
autoFocus
defaultValue={todo.do}
ref="editItem"
onKeyUp = {(e)=>{this.editedHandle(e,index)}}
/>
</li>
// 进入编辑状态
editTodoHandle = (item)=>{
this.refs.autofocus = true
document.querySelectorAll('.edit')[0].autofocus = true
document.querySelectorAll('.edit')[1].autofocus = true
document.querySelectorAll('.edit')[2].autofocus = true
this.setState({
newTodo:item
})
}
// 编辑完成
editedHandle(e,index){
// 如果是按退出键退出编辑状态=27
if(e.keyCode==27){
this.setState({
newTodo:{}
})
return;
}
if(e.keyCode==13){
let todos = this.state.todos.map((item,i)=>{
if(i==index){
item.do = e.target.value
}
return item
})
this.setState({
todos,
newTodo:{}
})
}
}
点击叉号.destroy
按钮删除该条数据
- 绑定事件
onClick={()=>{this.delTodoHandle}}
- 点击删除该条数据(处理函数)
delTodoHandle(index){
let todos = this.state.todos
todos.splice(index,1)
// 保存数据
this.setState({
todos
})
}
解决现实item left
未完成的数量
<span className="todo-count"><strong>{this.state.todos.filter((t)=>!t.bool).length}</strong>item left</span>
清除已完成Clear completed
- 绑定事件
<button className="clear-completed" onClick={this.clearCompletedHandle}>Clear completed</button>
- 清除已完成事件
clearCompletedHandle(){
let todos = this.state.todos.filter(t=>!t.bool)
this.setState({
todos
})
}
选中完成,未完成,所有的项目completed,active,all
- 监听路由的变化(
window.location.hash
) - 我们可以返回一个值,让todo每一项遍历是我们返回的值,而不是
todos
(当然也可以直接操作设置todos
数据setState
,不过要考虑改变了原来的数据todos
) - 定义一个数组存放todos数据
this.state = {
todos:props.data,
newTodo:{},
newTodos:[] //用来拷贝todos
}
- 定义一个函数处理路由规则,判断路由的哈希值
hashChangeHandle(){
// console.log(location.hash.startsWith('/active',1))
// 这是正在完成项目,即未完成(bool:false)
let newTodos = []
if(location.hash.startsWith('/active',1)){
newTodos = this.state.todos.filter(t=>!t.bool)
}else if(location.hash.startsWith('/completed',1)){
newTodos = this.state.todos.filter(t=>t.bool)
}else{
newTodos = this.state.todos
}
this.setState({
newTodos
})
}
- 初始化就调用它(利用React的生命周期函数)
componentWillMount(){
this.hashChangeHandle()
}
- 监听hash值的改变
componentDidMount(){
window.onhashchange = function(){
// console.log(this)
this.hashChangeHandle()
}.bind(this)
}
- 类的高亮,根据路由变化给类添加选中样式,这个比吃炒面还简单…
<ul className="filters">
<li>
<a className={"/"==window.location.hash.substring(1)?"selected":""} href="#/" >All</a>
</li>
<li>
<a className={"/active"==window.location.hash.substring(1)?"selected":""} href="#/active">Active</a>
</li>
<li>
<a className={"/completed"==window.location.hash.substring(1)?"selected":""} href="#/completed">Completed</a>
</li>
</ul>
添加一条数据
<input
className="new-todo"
ref="addTodo"
placeholder="What needs to be done?"
autoFocus={true}
onKeyUp={(e)=>{this.addTodoHandle(e)}}/>
addTodoHandle(e){
//如果文本框为空或则不是使用enter不执行任何操作
let addVal = this.refs.addTodo.value
let todos = this.state.todos
if(e.keyCode!==13 || !addVal.length){
return;
}
let id = todos.length?todos[todos.length-1].id+1:1
let todo = {
id,
do:addVal,
bool:false
}
todos.push(todo)
this.setState({
todos,
newTodos:todos
})
// 清空
this.refs.addTodo.value = ''
}
全选与反选
- 先实现反选:即所有todo都是完成completed时,我们的箭头才是高亮,其余情况不高亮
- 我们首先使用defaultChecked进行绑定,然而,该属性只在初始的时候有用,怎么改变todo的选择都没用
<input id="toggle-all" className="toggle-all" type="checkbox" defaultChecked={this.state.newTodos.every(t=>t.bool)} ref="selectedCheck"/>
- 使用
checkedLink={}
又会报错,只能使用js操作checked
来实现反选
changeCheckHandle = (index)=>{
let todos = this.state.todos.map((item,i)=>{
if(i==index){
item.bool = !item.bool
}
return item
})
this.setState({
todos,
newTodos:todos
})
this.refs.selectedCheck.checked=this.state.newTodos.every(t=>t.bool)
}
4. 全选:即点击三角图标,如果它高亮,所有的todo项都是完成
// 全选
toggleHandle = ()=>{
let todos = this.state.todos
let toggle = document.querySelectorAll('.toggle')
todos.forEach(item => {
item.bool = this.refs.selectedCheck.checked
})
for(var i=0;i< toggle.length;i++){
toggle[i].checked = this.refs.selectedCheck.checked
}
this.setState({
todos,
newTodos:todos
})
}
<input className="toggle" type="checkbox" onChange={()=>{this.changeCheckHandle(index)}} defaultChecked={todo.bool}/>
数据持久化(本地储存)
- 获取本地储存的todos没有则定义为空数组(main.js)
let todos = JSON.parse(localStorage.getItem('todos') || '[]')
ReactDom.render(<App data={todos}></App>,document.querySelector('#app'))
- 当todos数据有变化时,立刻储存
shouldComponentUpdate(nextProps,nextState){
localStorage.setItem('todos',JSON.stringify(nextState.todos))
return true
}
bug解决
- 我们使用了newTodos,那么清除已完成的也要设置newTodos
clearCompletedHandle(){
// console.log(this.state)
let todos = this.state.todos.filter(t=>!t.bool)
this.setState({
todos,
newTodos:todos
})
}
<button className="clear-completed" onClick={()=>{this.clearCompletedHandle()}}>Clear completed</button>
- 当添加一条数据时,三角按钮
#toggle
不会跟我们想象那样变化
shouldComponentUpdate(nextProps,nextState){
this.refs.selectedCheck.checked=this.state.newTodos.every(t=>t.bool)
return true
}
- 编辑状态
editedHandle(e,index){
.......
this.setState({
todos,
newTodos:todos,
newTodo:{}
})
}
}
- 因为我没做过测试,所以可能有其它bug存在…