个人待办事项记录簿
简介:
- 这是基于todoMVC模板的一个记录待办事项页面,模板地址。
- 该模板需要自行通过npm下载初始化页面样式,而且没有各种交互的逻辑代码。
- 该项目的交互代码是采用原生js结合es6的数组方法实现。
- 虽然项目要实现的功能多,但实现起来都比较简单,非常适合练手。
实现的功能
- 待办事项的完成、添加、删除、全选和反选所有待办等功能,单击重新编辑待办功能、实时显示剩余未完成待办的功能、三个按钮的事项筛选功能、将所有事项存储到客户端中、一键清除所有已完成事项功能;
- 利用单一数据流的特点对项目进行开发。
展示
具体实现逻辑
- 事项的渲染(事项的刷新)
-
这里需要实现事项的初始化渲染和事项的刷新
-
在li里自定义一个属性来记录当前渲染的这条数据的id,即data-id=${randerData[i].id}
-
li标签里的completed类名是用于控制待办事项是否完成的样式的,如果添加completed类名,那么该li标签就有待办事项完成的css样式,该类名用数据的done属性进行控制,该属性也用于控制input单选框是否选中
-
初始化渲染只要对数据遍历,然后将拼接的字符串插入页面,
-
刷新事项就需要每次置空标签和字符串。
-
-
// 数据渲染函数
function rander(data) {
if (data.length == 0) {
return;
}
let str = '';
let count = 0;
let randerData = null;
oTodoList.innerHTML = '';
// 对数据进行筛选
switch (location.hash) {
// 待办
case '#/active':
randerData = data.filter(function (value, index) {
return !value.done;
})
break;
// 已完成
case '#/completed':
randerData = data.filter(function (value, index) {
return value.done;
})
break;
default:
randerData = data;
}
for (let i = 0; i < randerData.length; i++) {
str += `
<li data-id=${randerData[i].id} class=${randerData[i].done ? "completed" : ""} >
<div class="view">
<input class="toggle" type="checkbox" ${randerData[i].done ? "checked" : ''}>
<label>${randerData[i].todo}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${randerData[i].todo}">
</li>`
}
// 展示剩余未完成待办数量的功能
randerData.forEach(value => {
if (value.done == false) {
count++;
}
})
oTodoCount.innerHTML = count;
oTodoList.innerHTML = str;
}
-
利用事件冒泡原理给事项的父级ul绑定点击事件,然后根据事件对象里记录DOM不同的class类值来实现待办事项的完成、删除、单击编辑待办功能
- 待办事项的点击完成功能(点击对象为CheckBox单选框)(即事件源对象里记录的DOM的class类为toggle)
- 待办事项的选中和不选中的切换功能:获取事件源DOM对象的li父级上记录的id值,根据这个id值找到data里的数据,将这条数据的done赋值为事件源DOM对象的checked的值,然后重新渲染修改后data。(checked的值用于控制li标签的class类名completed的有无,class类名值为completed就会让li标签具有待办已完成的样式)然后重新渲染data。
- 待办事项的点击完成功能(点击对象为CheckBox单选框)(即事件源对象里记录的DOM的class类为toggle)
// 记录当前点击到的li的索引
let id = e.target.parentElement.parentElement.getAttribute('data-id');
let temp = data.find(value => value.id == id);
// 只将完成待办的复选框点击事件冒泡执行
if (e.target.className == 'toggle') {
// 待办事项完成,即将点击到的复选框选中,然后再重新渲染
temp.done = e.target.checked;
rander(data);
}
-
待办事项的点击删除功能(点击对象为删除按钮)(即事件源对象里记录的DOM的class类为destroy)
- 获取事件源DOM对象的li父级上记录的id值,根据这个id值找到data里的数据,利用indexOf获取到这条数据在data里的索引,然后利用数组方法splice剪切掉这条数据,再重新渲染修改后data。
// 删除待办功能
if (e.target.className == 'destroy') {
data.splice(data.indexOf(temp), 1);
rander(data);
}
-
待办事项的点击重新编辑功能(点击对象为label标签)(即事件源对象里记录的DOM的标签名为LABEL)
- 给当前点击到的label标签的父级li标签加上editing的class类名(以拼接字符串的方式)。
// 单击可以编辑待办
if (e.target.tagName == 'LABEL') {
// 待办编辑框展示
e.target.parentElement.parentElement.className += ' editing';
// 让文本框自动选中
let oInput = e.target.parentElement.parentElement.getElementsByClassName('edit')[0];
oInput.focus();
// 调整输入框的光标到文字最右边
oInput.setSelectionRange(-1, -1);
}
- 当修改完成后就需要将这个修改后的值赋值到data数组的todo里:首先需要给class类名为todo-list绑定一个回车事件,再获取到事件源对象上的DOM对象,然后去找父级的li利用getAttridute()来获取到保存的id值,再在data数组里找到这条数据,并将它的todo值,当前这个input框的value值,然后重新渲染data。
oTodoList.addEventListener('keyup', (e) => {
// 获取当前选中的重新编辑输入框的索引
let id = e.target.parentElement.getAttribute('data-id');
// 根据索引找到待办项li
let temp = data.find(function (value) {
return value.id == id;
})
// 将重新编辑过的待办项进行赋值
if (e.target.className == 'edit' && e.keyCode == 13) {
temp.todo = e.target.value;
rander(data);
}
})
- 待办事项的全选和反选功能
-
给toggle-all的label标签绑定change事件,该事件是用于监听input框聚焦和失焦的变化。
-
只需要将与label绑定的input标签的checked值赋值到data里的每条数据的done的值中,然后重新渲染data,就可以根据一个按钮实现一键全选和反选的功能。
-
// 实现待办的全选和反选功能
oToggleAll.addEventListener('change', () => {
data.forEach(value => value.done = oToggleAll.checked);
rander(data)
})
- 因为每完成一个待完成都有可能触发全选功能或全不选功能,所以就需要在每次完成一项待办任务时都要对data里的每条数据的done值进行检测,如果所有的事项全部完成就要让input上的checked为true,否则就为false。
// 当先代办完成后判断是否触发全选按钮
if (data.every(value => value.done) == true) {
oToggleAll.checked = true;
} else {
oToggleAll.checked = false;
}
-
添加待办功能
- 给class类名为new-todo的input标签绑定回车事件,然后在data的最后一位添加一个新数据,todo值为输入的input的value值,然后重新渲染data,创建完毕后需要清空input的value值。
// 添加待办功能
oNewTodo.addEventListener('keyup', e => {
if (e.key == 'Enter') {
let obj = {
id: data.length == 0 ? 1 : data[data.length - 1].id + 1,
todo: e.target.value,
done: false,
}
data.push(obj);
rander(data);
// 待办创建完毕清除输入框
e.target.value = '';
}
})
-
3个按钮的筛选功能
- 3个按钮的样式改变
- 在这个下载的模板里这几个按钮的功能都是a标签以hash跳转的方式来写的,所以给window绑定监听页面以hash跳转就触发的hashchange事件。
- 遍历这3个a标签的hash值是否与当前显示页面的hash值相同,相同就要给这个a标签加上一个值为selected的class类名,该类名是用于控制选中状态样式
- 3个按钮的样式改变
// 三个按钮的筛选功能
window.onhashchange = function () {
// a标签样式class类名的修改
Array.from(oFiltersList).forEach(function (value, index) {
if (value.hash == location.hash) {
document.getElementsByClassName('selected')[0].className = '';
oFiltersList[index].className = 'selected';
}
})
// 重新渲染数组
rander(data);
}
-
筛选功能的实现
- 该功能不需要再去写方法去实现,只需要在事项渲染阶段时就对事项进行根据当前打开页面的hash值来判断需要展示什么类型的数据,然后用数组的filter方法对数组进行筛选。
// 对数据进行筛选
switch (location.hash) {
// 待办
case '#/active':
randerData = data.filter(function (value, index) {
return !value.done;
})
break;
// 已完成
case '#/completed':
randerData = data.filter(function (value, index) {
return value.done;
})
break;
default:
randerData = data;
}
-
一键清除完成事项功能
- 只需要将data里done状态为false的都筛选出来,然后把这些重新筛选出来的数据进行渲染。
// 一键清除完成事项功能
oClearCompleted.onclick = function (e) {
data = data.filter(function (value) {
return !value.done;
})
rander(data);
}
-
将数据存储到客户端功能
- 以localStorage.setItem(‘todoList’, JSON.stringify(data))的形式将数据存储到localstorage中,并且在每次重新渲染data时都要存储一次
-
给修改待办的input框绑定一个失焦事件
- 该事件绑定在class类名为todo-list的标签上,所以不能绑定blur事件,但可用focuout代替。当失焦时将当前li上editing的类名删掉。
// 取消重新编辑待办项的聚焦事件,blur失焦事件无法冒泡,可用focusout代替
oTodoList.addEventListener('focusout', function (e) {
if (e.target.className == 'edit') {
// 将li上editing的类名删掉
let str = e.target.parentElement.getAttribute('class')
e.target.parentElement.className = str.replace(' editing', '')
}
})
rander(data);
}
html部分:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="创建一个待办事项" autofocus>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
</section>
<footer class="footer">
<span class="todo-count"><strong> 0 </strong> 件未完成</span>
<ul class="filters">
<li>
<a class="selected" href="#/">所有事项</a>
</li>
<li>
<a href="#/active">待办事项</a>
</li>
<li>
<a href="#/completed">完成事项</a>
</li>
</ul>
<button class="clear-completed">清除完成事项</button>
</footer>
</section>
<script src="js/app.js"></script>
</body>
</html>
javaScript部分
window.onload = () => {
let data;
let dataStr = localStorage.getItem('todoList');
// 获取要渲染的数据
if (dataStr) {
data = JSON.parse(dataStr);
} else {
data = [];
}
let oTodoList = document.getElementsByClassName('todo-list')[0];
let oToggleAll = document.getElementsByClassName('toggle-all')[0];
let oNewTodo = document.getElementsByClassName('new-todo')[0];
let oTodoCount = document.getElementsByClassName('todo-count')[0].getElementsByTagName('strong')[0];
let oFiltersList = document.getElementsByClassName('filters')[0].getElementsByTagName('a');
let oClearCompleted = document.getElementsByClassName('clear-completed')[0];
// 数据渲染函数
function rander(data) {
if (data.length == 0) {
return;
}
let str = '';
let count = 0;
let randerData = null;
oTodoList.innerHTML = '';
// 对数据进行筛选
switch (location.hash) {
// 待办
case '#/active':
randerData = data.filter(function (value, index) {
return !value.done;
})
break;
// 已完成
case '#/completed':
randerData = data.filter(function (value, index) {
return value.done;
})
break;
default:
randerData = data;
}
for (let i = 0; i < randerData.length; i++) {
str += `
<li data-id=${randerData[i].id} class=${randerData[i].done ? "completed" : ""} >
<div class="view">
<input class="toggle" type="checkbox" ${randerData[i].done ? "checked" : ''}>
<label>${randerData[i].todo}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${randerData[i].todo}">
</li>`
}
// 展示剩余未完成待办数量的功能
randerData.forEach(value => {
if (value.done == false) {
count++;
}
})
oTodoCount.innerHTML = count;
oTodoList.innerHTML = str;
}
// 给ul绑定事件,将li里的事件冒泡执行
oTodoList.addEventListener('click', (e) => {
// 记录当前点击到的li的索引
let id = e.target.parentElement.parentElement.getAttribute('data-id');
let temp = data.find(value => value.id == id);
// 只将完成待办的复选框点击事件冒泡执行
if (e.target.className == 'toggle') {
// 待办事项完成,即将点击到的复选框选中,然后再重新渲染
temp.done = e.target.checked;
// 当先代办完成后判断是否触发全选按钮
if (data.every(value => value.done) == true) {
oToggleAll.checked = true;
} else {
oToggleAll.checked = false;
}
// 当数据发生修改就要把数据存储到localStorage中
localStorage.setItem('todoList', JSON.stringify(data));
rander(data);
}
// 删除待办功能
if (e.target.className == 'destroy') {
data.splice(data.indexOf(temp), 1);
// 当数据发生修改就要把数据存储到localStorage中
localStorage.setItem('todoList', JSON.stringify(data));
rander(data);
}
// 单击可以编辑待办
if (e.target.tagName == 'LABEL') {
// 待办编辑框展示
e.target.parentElement.parentElement.className += ' editing';
// 让文本框自动选中
let oInput = e.target.parentElement.parentElement.getElementsByClassName('edit')[0];
oInput.focus();
// 调整输入框的光标到文字最右边
oInput.setSelectionRange(-1, -1);
}
})
oTodoList.addEventListener('keyup', (e) => {
// 获取当前选中的重新编辑输入框的索引
let id = e.target.parentElement.getAttribute('data-id');
// 根据索引找到待办项li
let temp = data.find(function (value) {
return value.id == id;
})
// 将重新编辑过的待办项进行赋值
if (e.target.className == 'edit' && e.keyCode == 13) {
temp.todo = e.target.value;
// 当数据发生修改就要把数据存储到localStorage中
localStorage.setItem('todoList', JSON.stringify(data));
rander(data);
}
})
// 实现待办的全选和反选功能
oToggleAll.addEventListener('change', () => {
data.forEach(value => value.done = oToggleAll.checked);
// 当数据发生修改就要把数据存储到localStorage中
localStorage.setItem('todoList', JSON.stringify(data));
rander(data)
})
// 添加待办功能
oNewTodo.addEventListener('keyup', e => {
if (e.key == 'Enter') {
let obj = {
id: data.length == 0 ? 1 : data[data.length - 1].id + 1,
todo: e.target.value,
done: false,
}
data.push(obj);
// 当数据发生修改就要把数据存储到localStorage中
localStorage.setItem('todoList', JSON.stringify(data));
rander(data);
// 待办创建完毕清除输入框
e.target.value = '';
}
})
// 三个按钮的筛选功能
window.onhashchange = function () {
// a标签样式class类名的修改
Array.from(oFiltersList).forEach(function (value, index) {
if (value.hash == location.hash) {
document.getElementsByClassName('selected')[0].className = '';
oFiltersList[index].className = 'selected';
}
})
// 重新渲染数组
rander(data);
}
// 一键清除完成事项功能
oClearCompleted.onclick = function (e) {
data = data.filter(function (value) {
return !value.done;
})
// 当数据发生修改就要把数据存储到localStorage中
localStorage.setItem('todoList', JSON.stringify(data));
rander(data);
}
// 取消重新编辑待办项的聚焦事件,blur失焦事件无法冒泡,可用focusout代替
oTodoList.addEventListener('focusout', function (e) {
if (e.target.className == 'edit') {
// 将li上editing的类名删掉
let str = e.target.parentElement.getAttribute('class')
e.target.parentElement.className = str.replace(' editing', '')
}
})
rander(data);
}