Todos Data

-- 待办应用(数据版本)

课程概要

本课程主要讲解 JavaScript 数据维护和使用,概括了 JavaScript 单例模式中的数据维护和使用,以及数组的方法,例如:forEach、map、filter 。通过本课程,完成一个数据版 Todos 待办应用,大家将对数据维护和使用,以及有数组的方法更深入的了解。

主要包含以下功能:

  • 包含之前小节的添加、切换、删除功能
  • 添加筛选显示不同状态的功能( 全部、待办、已办 )
  • 视图的数据更具 PAGE.data 中的数据来同步相应

知识点

本课程涉及到的主要知识点有:

  1. innerHTML 设置或获取 HTML 元素内容
// 获取,获取整个 body 中的HTML元素内容
let bodyElement = document.body.innerHTML;

// 设置,将body 中的HTML元素内容替换为 h1 标签的 Hello world。
document.body.innerHTML = '<h1>Hello, Word!</h1>';
  1. dataset 自定义数据属性
<div id="ele" data-hi="hello,world"> Hi ~ </div>
let element = document.getElementById('ele');
let dataset = element.dataset; // { hi: 'Hello, World!'}
  1. join 数组转字符串
let arr = [1,2,3,4];
let str = arr.join('-'); // '1-2-3-4'
  1. 模版字符串 模板字符串使用反引号 () 来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。
let tmp = 'world';
let str = `Hello ${tmp}`; // Hello world
  1. forEach 对数组中每一项进行运行指定函数,没有返回值。
let arr = [1,2,3];
arr.forEach(function(data,index){
  console.log('当前项目:', data)
  console.log('当前索引:', index)
})
  1. map 对数组中的每一项进行运行指定的函数,返回每一项结果返回的数组。
let arr = [1,2,3];
let newArr = arr.map(function(data,index){
  return data * data
})

// [1,4,9];
  1. filter 对数组中每一项进行运行指定函数,返回该函数会返回 true 的项组成的数组。
let arr = [1,2,3,4,5,6];
let newArr = arr.map(function(data,index){
  return data * data
})

// [1,4,9];
  1. Number 把其他类型的值转化成数字类型
let str = '42';
let bol = true;
let obj = {};
Number(str); // 42
Number(bol); // 1
Number(obj); // NaN
  1. Object.keys 返回对象属性组成的数组
const obj = {
  name: 'Jax',
  age: '18'
}

Object.keys(obj); // ['name', 'age']
  1. 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 中的数据。
  1. splice 修改数组

splice,方法收三个参数,第一个为修改开始的位置,二个参数为删除的长度,第二以后的项目为插入数组新增的项目。函数返回,删除的项目数组。

let arr = [1,2,3,4];
let tmp = arr.splice(1,1);

// arr [1,3,4];
// tmp [2]
  1. 箭头函数

就语法不通,function 的另外的写法,注意的事 this 的指向不同,在之后的知识点中会涉及到。

let sumFunc1 = function(a,b) {
  return a + b
}

let sumFunc2 = (a,b) => {
  return a + b
}

// 简写一行并去掉大括号会默认 return 简写值
let sumFunc3 = (a,b) => a + b;

实现步骤

  1. HTML & CSS
  2. 构建单例子框架对象
  3. 把数据渲染到视图
  4. 绑定添加事件
  5. 绑定修改事件
  6. 绑定删除事件
  7. 绑定筛选事件

HTML & CSS

  1. 在上一节 HTML 的基础上添加筛选控制的 HTML
  2. 在上一节 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;
}

构建单例子框架对象

  1. 定义 PAGE 对象
  2. 创建 init 、 bind 方法,在 init 中调用 bind
  3. 调用 PAGE.init
  4. 定义 PAGE.data 数据集合
  5. 定义 PAGE.data.todos 用于储存 todos 数组数据,其每一项包含 title 内容及 completed 完成状态属性
  6. 定义 PAGE.data.filters 用于存储当前拥有什么筛选规则,包含 全部、待办、已办。
  7. 定义 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();

把数据渲染到视图

  1. 在 init 方法中调用 render 渲染方法
  2. 在 PAGE 中定义 render 渲染方法
  3. 获取 PAGE.data.todos 应用的数据
  4. 获取 PAGE.data.filters 所拥有的筛选规则
  5. 获取 PAGE.data.filter 获取当前的筛选规则
  6. 为应用数据添加索引
  7. 根据筛选规则生成显示数组
  8. 根据显示数组生成显示事项 HTML 的字符串,并加上 data-index 索引 ,用于之后的事件使用寻找当前元素对应的数据。
  9. 根据筛选规则生成筛选规则 HTML 的字符串,并加上 data-id 索引 ,用于之后的事件使用寻找当前元素对应的数据。
  10. 显示事项 HTML 的字符串替换到 todos-list 元素中
  11. 筛选规则 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();

绑定添加事件

  1. 为输入元素的绑定事件
  2. 判断键位和输入值
  3. 获取 todos 并在数组中添加一项
  4. 重新渲染视图
  5. 输入框的值设置为空
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();

绑定修改事件

  1. 添加委托绑定封装函数
  2. 委托绑定切换事件 toggleTodo
  3. 创建 toggleTodo 事件
  4. 获取 todos 数组、父级元素、位置索引
  5. 修改 todos 索引下项目 completed 完成属性的值
  6. 重新渲染视图
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();

绑定删除事件

  1. 委托绑定删除事件 removeTodo
  2. 创建 removeTodo 事件
  3. 获取 todos 数组、父级元素、位置索引
  4. 删除 todos 索引下项目
  5. 重新渲染视图
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();

绑定筛选事件

  1. 委托绑定筛选事件 filterTodo
  2. 创建 filterTodo 事件
  3. 获取筛选规则
  4. 更新筛选规则
  5. 重新渲染视图
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>