Todos Data
-- 待办应用(数据版本)
课程概要
本课程主要讲解 JavaScript 数据维护和使用,概括了 JavaScript 单例模式中的数据维护和使用,以及数组的方法,例如:forEach、map、filter 。通过本课程,完成一个数据版 Todos 待办应用,大家将对数据维护和使用,以及有数组的方法更深入的了解。
主要包含以下功能:
- 包含之前小节的添加、切换、删除功能
- 添加筛选显示不同状态的功能( 全部、待办、已办 )
- 视图的数据更具 PAGE.data 中的数据来同步相应
知识点
本课程涉及到的主要知识点有:
- innerHTML 设置或获取 HTML 元素内容
// 获取,获取整个 body 中的HTML元素内容
let bodyElement = document.body.innerHTML;
// 设置,将body 中的HTML元素内容替换为 h1 标签的 Hello world。
document.body.innerHTML = '<h1>Hello, Word!</h1>';
- dataset 自定义数据属性
<div id="ele" data-hi="hello,world"> Hi ~ </div>
let element = document.getElementById('ele');
let dataset = element.dataset; // { hi: 'Hello, World!'}
- join 数组转字符串
let arr = [1,2,3,4];
let str = arr.join('-'); // '1-2-3-4'
- 模版字符串
模板字符串使用反引号 (
) 来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。
let tmp = 'world';
let str = `Hello ${tmp}`; // Hello world
- forEach 对数组中每一项进行运行指定函数,没有返回值。
let arr = [1,2,3];
arr.forEach(function(data,index){
console.log('当前项目:', data)
console.log('当前索引:', index)
})
- map 对数组中的每一项进行运行指定的函数,返回每一项结果返回的数组。
let arr = [1,2,3];
let newArr = arr.map(function(data,index){
return data * data
})
// [1,4,9];
- filter 对数组中每一项进行运行指定函数,返回该函数会返回 true 的项组成的数组。
let arr = [1,2,3,4,5,6];
let newArr = arr.map(function(data,index){
return data * data
})
// [1,4,9];
- Number 把其他类型的值转化成数字类型
let str = '42';
let bol = true;
let obj = {};
Number(str); // 42
Number(bol); // 1
Number(obj); // NaN
- Object.keys 返回对象属性组成的数组
const obj = {
name: 'Jax',
age: '18'
}
Object.keys(obj); // ['name', 'age']
- PAGE.data 数据的引用和修改
const PAGE = {
data: {
name: 'Jax'
}
}
PAGE.data.name; // Jax
PAGE.data.name = 'Jeo';
PAGE.data.name; // Jeo
// 把我们的数据放在 PAGE.data 中,然后通过 PAGE.data 获取和修改,维护 PAGE.data 中的数据。
- splice 修改数组
splice,方法收三个参数,第一个为修改开始的位置,二个参数为删除的长度,第二以后的项目为插入数组新增的项目。函数返回,删除的项目数组。
let arr = [1,2,3,4];
let tmp = arr.splice(1,1);
// arr [1,3,4];
// tmp [2]
- 箭头函数
就语法不通,function 的另外的写法,注意的事 this 的指向不同,在之后的知识点中会涉及到。
let sumFunc1 = function(a,b) {
return a + b
}
let sumFunc2 = (a,b) => {
return a + b
}
// 简写一行并去掉大括号会默认 return 简写值
let sumFunc3 = (a,b) => a + b;
实现步骤
- HTML & CSS
- 构建单例子框架对象
- 把数据渲染到视图
- 绑定添加事件
- 绑定修改事件
- 绑定删除事件
- 绑定筛选事件
HTML & CSS
- 在上一节 HTML 的基础上添加筛选控制的 HTML
- 在上一节 CSS 的基础上添加筛选控制的 CSS
<div class="todos-container">
<h1 class="todos-title">todos</h1>
<div class="todos-content">
<div class="todos-input-cell">...</div>
<div id="todos-list" class="todos-list">...</div>
<!-- 筛选控制 -->
<div class="todos-filter" id="todos-filter">
<span class="filter-item active">全部</span>
<span class="filter-item">待办</span>
<span class="filter-item">已办</span>
</div>
<!-- 筛选控制 -->
</div>
</div>
/* 筛选控制 */
.todos-filter{
border-top: 1px solid #e4e4e4;
padding: 8px 16px;
text-align: center;
}
.todos-filter .filter-item{
display: inline-block;
margin: 0 10px;
padding: 4px 8px;
font-size: 14px;
color: #999;
border: 1px solid #999;
border-radius: 4px;
cursor: pointer;
}
.todos-filter .filter-item.active{
border-color: #333;
color: #333;
}
构建单例子框架对象
- 定义 PAGE 对象
- 创建 init 、 bind 方法,在 init 中调用 bind
- 调用 PAGE.init
- 定义 PAGE.data 数据集合
- 定义 PAGE.data.todos 用于储存 todos 数组数据,其每一项包含 title 内容及 completed 完成状态属性
- 定义 PAGE.data.filters 用于存储当前拥有什么筛选规则,包含 全部、待办、已办。
- 定义 PAGE.data.filter 用于定义当前按照什么规则来进行数据筛选
// 1. 定义 PAGE 对象
const PAGE = {
// 4. 定义 PAGE.data 数据集合
data: {
todos: [{
title: '打一瓶酱油',
completed: false
},{
title: '跑步800米',
completed: true
}],
filters: {
1: '全部',
2: '待办',
3: '已办',
},
filter: 1,
},
// 2. 创建 init 、 bind 方法,在 init 中调用 bind
init: function() {
this.bind();
},
bind: function() {
}
}
// 3. 调用 PAGE.init
PAGE.init();
把数据渲染到视图
- 在 init 方法中调用 render 渲染方法
- 在 PAGE 中定义 render 渲染方法
- 获取 PAGE.data.todos 应用的数据
- 获取 PAGE.data.filters 所拥有的筛选规则
- 获取 PAGE.data.filter 获取当前的筛选规则
- 为应用数据添加索引
- 根据筛选规则生成显示数组
- 根据显示数组生成显示事项 HTML 的字符串,并加上 data-index 索引 ,用于之后的事件使用寻找当前元素对应的数据。
- 根据筛选规则生成筛选规则 HTML 的字符串,并加上 data-id 索引 ,用于之后的事件使用寻找当前元素对应的数据。
- 显示事项 HTML 的字符串替换到 todos-list 元素中
- 筛选规则 HTML 的字符串替换到 todos-filter 元素中
这样,我们就可以更具 todos 数据的不同,还有 filter 当前的规则来展示出试图。
const PAGE = {
data: {
todos: [{
title: '打一瓶酱油',
completed: false
},{
title: '跑步800米',
completed: true
}],
filters: {
1: '全部',
2: '待办',
3: '已办',
},
filter: 1,
},
init: function() {
this.bind();
// 1. 在 init 方法中调用 render 渲染方法
this.render();
},
bind: function() {
},
// 2. 在 PAGE 中定义 render 渲染方法
render: function() {
// 3. 获取 PAGE.data.todos 应用的数据
// 4. 获取 PAGE.data.filters 所拥有的筛选规则
// 5. 获取 PAGE.data.filter 获取当前的筛选规则
let todos = this.data.todos;
let filters = this.data.filters;
let filter = this.data.filter;
// 6. 为应用数据添加索引
todos.forEach((data,index)=> data.index = index);
// 7. 根据筛选规则生成显示数组
let showTodos;
switch (filter) {
case 2:
showTodos = todos.filter( data => !data.completed );
break;
case 3:
showTodos = todos.filter( data => data.completed );
break;
default:
showTodos = todos;
break
}
// 8. 根据显示数组生成显示事项 HTML 的字符串
// 并加上 data-index 索引 ,用于之后的事件使用寻找当前元素对应的数据。
let todosElement = showTodos.map((data)=>{
return `
<div class="todo-item ${data.completed ? 'active' : ''}" data-index="${data.index}">
<div class="todo-item-hd"></div>
<div class="todo-item-bd">${data.title}</div>
<div class="todo-item-ft">x</div>
</div>
`
}).join('');
// 9. 根据筛选规则生成筛选规则 HTML 的字符串
// 并加上 data-id 索引 ,用于之后的事件使用寻找当前元素对应的数据。
let filterElement = Object.keys(filters).map( key => {
return `<span class="filter-item ${filter == key ? 'active' : ''}" data-id="${key}">${filters[key]}</span>`
}).join('');
// 10. 显示事项 HTML 的字符串替换到 todos-list 元素中
// 11. 筛选规则 HTML 的字符串替换到 todos-filter 元素中
let todoList = document.getElementById('todos-list');
let todoFilter = document.getElementById('todos-filter');
todoList.innerHTML = todosElement;
todoFilter.innerHTML = filterElement;
}
}
PAGE.init();
绑定添加事件
- 为输入元素的绑定事件
- 判断键位和输入值
- 获取 todos 并在数组中添加一项
- 重新渲染视图
- 输入框的值设置为空
const PAGE = {
...,
bind: function() {
// 1. 为输入元素的绑定事件
let input = document.getElementById('todo-input');
input.addEventListener('keyup',this.addTodo);
},
...,
addTodo: function(e){
// 2. 判断键位和输入值
let value = this.value.trim();
if (e.which !== 13 || !value) {
return;
}
// 3. 获取 todos 并在数组中添加一项
let todos = PAGE.data.todos;
todos.push({
title: value,
completed: false
})
// 4. 重新渲染视图
PAGE.render();
// 5. 输入框的值设置为空
this.value = '';
}
}
PAGE.init();
绑定修改事件
- 添加委托绑定封装函数
- 委托绑定切换事件 toggleTodo
- 创建 toggleTodo 事件
- 获取 todos 数组、父级元素、位置索引
- 修改 todos 索引下项目 completed 完成属性的值
- 重新渲染视图
const PAGE = {
...,
bind: function() {
let input = document.getElementById('todo-input');
input.addEventListener('keyup',this.addTodo);
// 2. 委托绑定切换事件
let todoList = document.getElementById('todos-list');
this.onEventLister(todoList,'click','todo-item-hd',this.toggleTodo);
},
...,
// 1. 添加委托绑定封装函数
onEventLister: function(parentNode,action,childClassName,callback) {
parentNode.addEventListener(action,function(e){
e.target.className.indexOf(childClassName) >= 0 && callback(e);
})
},
// 3. 创建 toggleTodo 事件
toggleTodo: function(e) {
// 4. 获取 todos 数组、父级元素、位置索引
let todos = PAGE.data.todos;
let todoItem = e.target.parentNode;
let index = todoItem.dataset.index;
// 5. 修改 todos 索引下项目 completed 完成属性的值
todos[index].completed = !todos[index].completed;
// 6. 重新渲染视图
PAGE.render();
}
}
PAGE.init();
绑定删除事件
- 委托绑定删除事件 removeTodo
- 创建 removeTodo 事件
- 获取 todos 数组、父级元素、位置索引
- 删除 todos 索引下项目
- 重新渲染视图
const PAGE = {
...,
bind: function() {
let input = document.getElementById('todo-input');
input.addEventListener('keyup',this.addTodo);
let todoList = document.getElementById('todos-list');
this.onEventLister(todoList,'click','todo-item-hd',this.toggleTodo);
// 1. 委托绑定删除事件 removeTodo
this.onEventLister(todoList,'click','todo-item-ft',this.removeTodo);
},
...,
// 2. 创建 removeTodo 事件
removeTodo: function(e) {
// 3. 获取 todos 数组、父级元素、位置索引
let todos = PAGE.data.todos;
let todoItem = e.target.parentNode;
let index = todoItem.dataset.index;
// 4. 删除 todos 索引下项目
todos.splice(index,1);
// 5. 重新渲染视图
PAGE.render();
},
}
PAGE.init();
绑定筛选事件
- 委托绑定筛选事件 filterTodo
- 创建 filterTodo 事件
- 获取筛选规则
- 更新筛选规则
- 重新渲染视图
const PAGE = {
...,
bind: function() {
let input = document.getElementById('todo-input');
input.addEventListener('keyup',this.addTodo);
let todoList = document.getElementById('todos-list');
this.onEventLister(todoList,'click','todo-item-hd',this.toggleTodo);
this.onEventLister(todoList,'click','todo-item-ft',this.removeTodo);
// 1. 委托绑定筛选事件 filterTodo
let todoFilter = document.getElementById('todos-filter');
this.onEventLister(todoFilter,'click','filter-item',this.filterTodo);
},
...,
// 2. 创建 filterTodo 事件
filterTodo: function(e) {
// 3. 获取筛选规则
let filterItem = e.target;
let filter = filterItem.dataset.id;
// 4. 更新筛选规则
PAGE.data.filter = Number(filter);
// 5. 重新渲染视图
PAGE.render();
}
}
PAGE.init();
代码示例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todes</title>
<style type="text/css">
*{
margin: 0;
padding: 0;
font: 24px Helvetica, Arial, sans-serif;
font-weight: 400;
box-sizing: border-box;
color: #666;
}
.todos-container{
margin: 100px auto;
width: 550px;
}
.todos-title{
font-size: 100px;
text-align: center;
font-weight: 100;
color: rgba(175, 47, 47, 0.15);
margin-bottom: 20px;
}
.todos-content{
position: relative;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
}
.todos-input{
display: block;
width: 100%;
padding: 16px 16px 16px 60px;
border: none;
outline: none;
font-weight: 200;
}
.todo-item{
padding: 16px;
display: flex;
border-top: 1px solid #e4e4e4;
}
.todo-item-hd{
position: relative;
width: 28px;
height: 28px;
border: 1px solid #e4e4e4;
border-radius: 50%;
margin-right: 16px;
cursor: pointer;
}
.todo-item-bd{
flex: 1;
color: #333;
}
.todo-item-ft{
display: none;
cursor: pointer;
}
.todo-item:hover .todo-item-ft{
display: inline-block;
color: #999;
}
.todo-item.active .todo-item-hd:before{
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(175, 47, 47, 0.15);
}
.todo-item.active .todo-item-bd{
text-decoration: line-through;
color: #999;
}
/*筛选*/
.todos-filter{
border-top: 1px solid #e4e4e4;
padding: 8px 16px;
text-align: center;
}
.todos-filter .filter-item{
display: inline-block;
margin: 0 10px;
padding: 4px 8px;
font-size: 14px;
color: #999;
border: 1px solid #999;
border-radius: 4px;
cursor: pointer;
}
.todos-filter .filter-item.active{
border-color: #333;
color: #333;
}
</style>
</head>
<body>
<div class="todos-container">
<h1 class="todos-title">todos</h1>
<div class="todos-content">
<div class="todos-input-cell">
<input id="todo-input" class="todos-input" type="text" name="todo" placeholder="请输入计划事项">
</div>
<div id="todos-list" class="todos-list">
<div class="todo-item">
<div class="todo-item-hd"></div>
<div class="todo-item-bd">打一瓶酱油</div>
<div class="todo-item-ft">x</div>
</div>
<div class="todo-item active">
<div class="todo-item-hd"></div>
<div class="todo-item-bd">跑步800米</div>
<div class="todo-item-ft">x</div>
</div>
</div>
<div class="todos-filter" id="todos-filter">
<span class="filter-item active">全部</span>
<span class="filter-item">待办</span>
<span class="filter-item">已办</span>
</div>
</div>
</div>
<script type="text/javascript">
const PAGE = {
data: {
todos: [{
title: '打一瓶酱油',
completed: false
},{
title: '跑步800米',
completed: true
}],
filter: 1,
filters: {
1: '全部',
2: '待办',
3: '已办',
}
},
init: function() {
this.bind();
this.render();
},
bind: function() {
let input = document.getElementById('todo-input');
let todoList = document.getElementById('todos-list');
let todoFilter = document.getElementById('todos-filter');
input.addEventListener('keyup',this.addTodo);
this.onEventLister(todoList,'click','todo-item-hd',this.toggleTodo);
this.onEventLister(todoList,'click','todo-item-ft',this.removeTodo);
this.onEventLister(todoFilter,'click','filter-item',this.filterTodo);
},
onEventLister: function(parentNode,action,childClassName,callback) {
parentNode.addEventListener(action,function(e){
e.target.className.indexOf(childClassName) >= 0 && callback(e);
})
},
render: function() {
let todos = this.data.todos;
todos.forEach((data,index)=> data.index = index);
let filters = this.data.filters;
let filter = this.data.filter;
let showTodos;
switch (filter) {
case 2:
showTodos = todos.filter( data => !data.completed );
break;
case 3:
showTodos = todos.filter( data => data.completed );
break;
default:
showTodos = todos;
break
}
let todosElement = showTodos.map( data =>{
return `
<div class="todo-item ${data.completed ? 'active' : ''}" data-index="${data.index}">
<div class="todo-item-hd"></div>
<div class="todo-item-bd">${data.title}</div>
<div class="todo-item-ft">x</div>
</div>
`
}).join('');
let filterElement = Object.keys(filters).map( key => {
return `<span class="filter-item ${filter == key ? 'active' : ''}" data-id="${key}">${filters[key]}</span>`
}).join('');
let todoList = document.getElementById('todos-list');
let todoFilter = document.getElementById('todos-filter');
todoList.innerHTML = todosElement;
todoFilter.innerHTML = filterElement;
},
addTodo: function(e){
let value = this.value.trim();
if (e.which !== 13 || !value) {
return;
}
let todos = PAGE.data.todos;
todos.push({
title: value,
completed: false
})
this.value = '';
PAGE.render();
},
toggleTodo: function(e) {
let todos = PAGE.data.todos;
let todoItem = e.target.parentNode;
let index = todoItem.dataset.index;
todos[index].completed = !todos[index].completed;
PAGE.render();
},
removeTodo: function(e) {
let todos = PAGE.data.todos;
let todoItem = e.target.parentNode;
let index = todoItem.dataset.index;
todos.splice(index,1);
PAGE.render();
},
filterTodo: function(e) {
let filterItem = e.target;
let filter = filterItem.dataset.id;
PAGE.data.filter = Number(filter);
PAGE.render();
}
}
PAGE.init();
</script>
</body>
</html>