AngularJS学习笔记之TodoMVC的分析


Posted in Javascript onFebruary 22, 2015

最近一段时间一直在看AngularJS,趁着一点时间总结一下。

官网地址:http://angularjs.org/

先推荐几个教程

1. AngularJS入门教程 比较基础,是官方Tutorial的翻译。

2. 七步从AngularJS菜鸟到专家 也比较基础,制作了一个在线音乐播放网站。

3. AngularJS开发指南 这个教程比较全面,但我感觉翻译的有些晦涩难懂。

看过这些教程后,觉得AngularJS也懂一点了,就想用它干点事,就分析一下AngularJS写的todomvc吧。

Todomvc官网地址:http://todomvc.com/

项目的目录如下:

bower_components里放了两个文件夹,其中angular文件夹是用来一如angular.js文件的,todomvc-common文件夹里的放入了所有todo项目统一的css\js(只是用来生成左侧内容的,与项目无关)和图片。

js文件夹是大头,里面放了相应的controller(控制器)\directive(指令)\service(服务)和app.js。

test文件夹里放的是测试用的代码,不分析。

index.html是项目的view页面。

先来看一下app.js

/*global angular */

/*jshint unused:false */

'use strict';

/**

 * The main TodoMVC app module

 *

 * @type {angular.Module}

 */

var todomvc = angular.module('todomvc', []);

就是定义了一个模块todomvc

再看一下services下的todoStorage.js

/*global todomvc */

'use strict';

/**

 * Services that persists and retrieves TODOs from localStorage

 */

todomvc.factory('todoStorage', function () {

    // todos JSON字符串存储的唯一标识

    var STORAGE_ID = 'todos-angularjs';

    return {

        // 从localStorage中取出todos,并解析成JSON对象

        get: function () {

            return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');

        },

        // 将todos对象转化成JSON字符串,并存入localStorage

        put: function (todos) {

            localStorage.setItem(STORAGE_ID, JSON.stringify(todos));

        }

    };

});

使用factory方法创建了todoStorage的service方法,这个service方法的本质就是返回了两个方法get和put,两者都是用了JSON2和HTML5的特性。get将todos的内容从localStorage中取出,并解析成JSON,put将todos转化成JSON字符串,并存储到localStorage中。

再看一下directives下面的两个指令文件。

todoFocus.js

/*global todomvc */

'use strict';

/**

 * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true

 */

todomvc.directive('todoFocus', function todoFocus($timeout) {

    return function (scope, elem, attrs) {

        // 为todoFocus属性的值添加监听

        scope.$watch(attrs.todoFocus, function (newVal) {

            if (newVal) {

                $timeout(function () {

                    elem[0].focus();

                }, 0, false);

            }

        });

    };

});

返回function的参数中,elem就是包含该指令的元素的数组,attrs是元素的所有属性、属性名等组成的对象。

其中用到了两个AngularJS的方法

$watch(watchExpression, listener, objectEquality) 注册一个侦听器回调,每当watchExpression变化时,监听回调将被执行。

$timeout(fn[, delay][, invokeApply]) 当timeout的值达到时,执行fn函数。

todoFocus.js创建了todoFocus指令。当一个元素拥有todoFocus属性时,该指令会为该元素的todoFocus属性的值添加监听,如果todoFocus属性的值改变成true,就会执行$timeout(function () {elem[0].focus();}, 0, false);其中的延迟时间为0秒,所以会立即执行elem[0].focus()。

todoEscape.js

/*global todomvc */

'use strict';

/**

 * Directive that executes an expression when the element it is applied to gets

 * an `escape` keydown event.

 */

todomvc.directive('todoEscape', function () {

    var ESCAPE_KEY = 27;

    return function (scope, elem, attrs) {

        elem.bind('keydown', function (event) {

            if (event.keyCode === ESCAPE_KEY) {

                scope.$apply(attrs.todoEscape);

            }

        });

    };

});

todoEscape.js创建了todoEscape指令。当按下Escape键时,执行attrs.todoEscape的表达式。

看一下大头,controllers文件夹中的todoCtrl.js,这个文件略长,我就直接写注释了。

/*global todomvc, angular */

'use strict';

/**

 * The main controller for the app. The controller:

 * - retrieves and persists the model via the todoStorage service

 * - exposes the model to the template and provides event handlers

 */

todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage, filterFilter) {

    // 从localStorage中获取todos

    var todos = $scope.todos = todoStorage.get();
    // 记录新的todo

    $scope.newTodo = '';

    // 记录编辑过的todo

    $scope.editedTodo = null;

    // 当todos的值改变时执行其中的方法

    $scope.$watch('todos', function (newValue, oldValue) {

        // 获取未完成的todos的数目

        $scope.remainingCount = filterFilter(todos, { completed: false }).length;

        // 获取已完成的todos的数目

        $scope.completedCount = todos.length - $scope.remainingCount;

        // 当且仅当$scope.remainingCount为0时,$scope.allChecked为true

        $scope.allChecked = !$scope.remainingCount;

        // 当todos的新值和旧值不相等时,向localStorage中存入todos

        if (newValue !== oldValue) { // This prevents unneeded calls to the local storage

            todoStorage.put(todos);

        }

    }, true);

    if ($location.path() === '') {

        // 如果$location.path()为空,就设置为/

        $location.path('/');

    }

    $scope.location = $location;

    // 当location.path()的值改变时执行其中的方法

    $scope.$watch('location.path()', function (path) {

        // 获取状态的过滤器

        // 如果path为'/active',过滤器为{ completed: false }

        // 如果path为'/completed',过滤器为{ completed: true }

        // 否则,过滤器为null

        $scope.statusFilter = (path === '/active') ?

            { completed: false } : (path === '/completed') ?

            { completed: true } : null;

    });

    // 添加一个新的todo

    $scope.addTodo = function () {

        var newTodo = $scope.newTodo.trim();

        if (!newTodo.length) {

            return;

        }

        // 向todos里添加一个todo,completed属性默认为false

        todos.push({

            title: newTodo,

            completed: false

        });

        // 置空

        $scope.newTodo = '';

    };

    // 编辑一个todo

    $scope.editTodo = function (todo) {

        $scope.editedTodo = todo;

        // Clone the original todo to restore it on demand.

        // 保存编辑前的todo,为恢复编辑前做准备

        $scope.originalTodo = angular.extend({}, todo);

    };

    // 编辑todo完成

    $scope.doneEditing = function (todo) {

        // 置空

        $scope.editedTodo = null;

        todo.title = todo.title.trim();

        if (!todo.title) {

            // 如果todo的title为空,则移除该todo

            $scope.removeTodo(todo);

        }

    };

    // 恢复编辑前的todo

    $scope.revertEditing = function (todo) {

        todos[todos.indexOf(todo)] = $scope.originalTodo;

        $scope.doneEditing($scope.originalTodo);

    };

    // 移除todo

    $scope.removeTodo = function (todo) {

        todos.splice(todos.indexOf(todo), 1);

    };

    // 清除已完成的todos

    $scope.clearCompletedTodos = function () {

        $scope.todos = todos = todos.filter(function (val) {

            return !val.completed;

        });

    };

    // 标记所有的todo的状态(true或false)

    $scope.markAll = function (completed) {

        todos.forEach(function (todo) {

            todo.completed = completed;

        });

    };

});

 最后看一下index.html,这个文件我们一段一段的分析。

<!doctype html>

<html lang="en" ng-app="todomvc" data-framework="angularjs">

    <head>

        <meta charset="utf-8">

        <meta http-equiv="X-UA-Compatible" content="IE=edge">

        <title>AngularJS • TodoMVC</title>

        <link rel="stylesheet" href="bower_components/todomvc-common/base.css">

        <style>[ng-cloak] { display: none; }</style>

    </head>

    <body>

        <section id="todoapp" ng-controller="TodoCtrl">

            <header id="header">

                <h1>todos</h1>

                <form id="todo-form" ng-submit="addTodo()">

                    <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" autofocus>

                </form>

            </header>

            <section id="main" ng-show="todos.length" ng-cloak>

                <input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">

                <label for="toggle-all">Mark all as complete</label>

                <ul id="todo-list">

                    <li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}">

                        <div class="view">

                            <input class="toggle" type="checkbox" ng-model="todo.completed">

                            <label ng-dblclick="editTodo(todo)">{{todo.title}}</label>

                            <button class="destroy" ng-click="removeTodo(todo)"></button>

                        </div>

                        <form ng-submit="doneEditing(todo)">

                            <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEditing(todo)" ng-blur="doneEditing(todo)" todo-focus="todo == editedTodo">

                        </form>

                    </li>

                </ul>

            </section>

            <footer id="footer" ng-show="todos.length" ng-cloak>

                <span id="todo-count"><strong>{{remainingCount}}</strong>

                    <ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>

                </span>

                <ul id="filters">

                    <li>

                        <a ng-class="{selected: location.path() == '/'} " href="#/">All</a>

                    </li>

                    <li>

                        <a ng-class="{selected: location.path() == '/active'}" href="#/active">Active</a>

                    </li>

                    <li>

                        <a ng-class="{selected: location.path() == '/completed'}" href="#/completed">Completed</a>

                    </li>

                </ul>

                <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button>

            </footer>

        </section>

        <footer id="info">

            <p>Double-click to edit a todo</p>

            <p>Credits:

                <a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,

                <a href="http://ericbidelman.com">Eric Bidelman</a>,

                <a href="http://jacobmumm.com">Jacob Mumm</a> and

                <a href="http://igorminar.com">Igor Minar</a>

            </p>

            <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>

        </footer>

        <script src="bower_components/todomvc-common/base.js"></script>

        <script src="bower_components/angular/angular.js"></script>

        <script src="js/app.js"></script>

        <script src="js/controllers/todoCtrl.js"></script>

        <script src="js/services/todoStorage.js"></script>

        <script src="js/directives/todoFocus.js"></script>

        <script src="js/directives/todoEscape.js"></script>

    </body>

</html>

首先是在最下面,引入相应的JS,这个就不多说了。

<script src="bower_components/todomvc-common/base.js"></script>

<script src="bower_components/angular/angular.js"></script>

<script src="js/app.js"></script>

<script src="js/controllers/todoCtrl.js"></script>

<script src="js/services/todoStorage.js"></script>

<script src="js/directives/todoFocus.js"></script>

<script src="js/directives/todoEscape.js"></script>

定义style[ng-cloak],含有ng-cloak属性则不可见。

<style>[ng-cloak] { display: none; }</style>

来看添加todo的html,绑定的model为newTodo,submit的方法是todoCtrl.js中的addTodo(),会添加一条todo,点击Enter,默认触发提交事件,就触发了addTodo()方法,添加了一条todo到todos中。

<form id="todo-form" ng-submit="addTodo()">

    <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" autofocus>

</form>

再看展示todos的html

<section id="main" ng-show="todos.length" ng-cloak>

    <input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">

    <label for="toggle-all">Mark all as complete</label>

    <ul id="todo-list">

        <li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}">

            <div class="view">

                <input class="toggle" type="checkbox" ng-model="todo.completed">

                <label ng-dblclick="editTodo(todo)">{{todo.title}}</label>

                <button class="destroy" ng-click="removeTodo(todo)"></button>

            </div>

            <form ng-submit="doneEditing(todo)">

                <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEditing(todo)" ng-blur="doneEditing(todo)" todo-focus="todo == editedTodo">

            </form>

        </li>

    </ul>

</section>

section使用ngShow方法根据todos的长度判断是否显示,加上ng-cloak属性是为了在刚开始时不要显示出AngularJS未处理的页面。可以去掉刷新试一试。

其中id为toggle-all的checkbox绑定到allChecked model上,点击触发markAll(allChecked),将allChecked的值传入,标记所有的todos。

使用ngRepeat循环产生li标签,todo in todos | filter:statusFilter track by $index,循环todos,用statusFilter过滤,用$index追踪。ngClass绑定了两个class,{completed: todo.completed, editing: todo == editedTodo},如果todo.completed为true,添加completed class,如果todo==editedTodo,则添加editing class。class为toggle的checkbox绑定到todo.completed。todo标题展示的label绑定了双击事件,双击触发editTodo(todo),editTodo会将todo赋给editedTodo,然后会触发下面form中的todoFocus指令,这时候form中的input可见。按Esc就触发revertEditing(todo),恢复到编辑前,按Enter或者失去焦点就触发doneEditing(todo) ,保存编辑后的todo。class为destroy的button绑定了click事件,点击触发removeTodo(todo),删除掉该条todo。

最后看todos的统计信息展示的html

<footer id="footer" ng-show="todos.length" ng-cloak>

    <span id="todo-count"><strong>{{remainingCount}}</strong>

        <ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>

    </span>

    <ul id="filters">

        <li>

            <a ng-class="{selected: location.path() == '/'} " href="#/">All</a>

        </li>

        <li>

            <a ng-class="{selected: location.path() == '/active'}" href="#/active">Active</a>

        </li>

        <li>

            <a ng-class="{selected: location.path() == '/completed'}" href="#/completed">Completed</a>

        </li>

    </ul>

    <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button>

</footer>

ng-pluralize标签实现了当remainingCount个数为1时,显示 item left,否则显示 items left。

id为filters的ul标签中根据location.path()的内容不同,标记不同的a标签被选中。

id为clear-completed的button添加了点击事件,触发clearCompletedTodos(),清除掉所有已完成的todo。

今天的笔记就先到这里吧,都是些个人心得,希望小伙伴们能够喜欢。

Javascript 相关文章推荐
jquery 可拖拽的窗体控件实现代码
Mar 21 Javascript
ImageZoom 图片放大镜效果(多功能扩展篇)
Apr 14 Javascript
基于jQuery实现表格数据的动态添加与统计的代码
Jan 31 Javascript
Jquery 选中表格一列并对表格排序实现原理
Dec 15 Javascript
如何使Chrome控制台支持多行js模式——意外发现
Jun 13 Javascript
JavaScript实现自动切换图片代码
Oct 11 Javascript
Angular组件化管理实现方法分析
Mar 17 Javascript
在Vue中使用echarts的方法
Feb 05 Javascript
Vue-cli@3.0 插件系统简析
Sep 05 Javascript
cnpm加速Angular项目创建的方法
Sep 07 Javascript
jquery 插件重新绑定的处理方法分析
Nov 23 jQuery
JS前后端实现身份证号验证代码解析
Jul 23 Javascript
使用jquery组件qrcode生成二维码及应用指南
Feb 22 #Javascript
javascript实现博客园页面右下角返回顶部按钮
Feb 22 #Javascript
JS+CSS实现感应鼠标渐变显示DIV层的方法
Feb 20 #Javascript
js实现图片和链接文字同步切换特效的方法
Feb 20 #Javascript
jQuery实现渐变弹出层和弹出菜单的方法
Feb 20 #Javascript
JavaScript获取文本框内选中文本的方法
Feb 20 #Javascript
jQuery常用数据处理方法小结
Feb 20 #Javascript
You might like
咖啡豆的最常见发酵处理方法,详细了解一下
2021/03/03 冲泡冲煮
一个典型的PHP分页实例代码分享
2011/07/28 PHP
CI框架学习笔记(一) - 环境安装、基本术语和框架流程
2014/10/26 PHP
PHP读取大文件的多种方法介绍
2016/04/04 PHP
php基于环形链表解决约瑟夫环问题示例
2017/11/07 PHP
jQuery的初始化与对象构建之浅析
2011/04/12 Javascript
jQuery EasyUI API 中文文档 - NumberSpinner数值微调器使用介绍
2011/10/21 Javascript
关闭浏览器时提示onbeforeunload事件
2013/12/25 Javascript
jquery 取子节点及当前节点属性值的方法
2014/08/24 Javascript
js防止页面被iframe调用的方法
2014/10/30 Javascript
详细解读AngularJS中的表单验证编程
2015/06/19 Javascript
JS实现的车标图片提示效果代码
2015/10/10 Javascript
JS实现超精简的链接列表在固定区域内滚动效果代码
2015/11/04 Javascript
轻松学习jQuery插件EasyUI EasyUI创建树形菜单
2015/11/30 Javascript
利用BootStrap弹出二级对话框的简单实现方法
2016/09/21 Javascript
Angular如何在应用初始化时运行代码详解
2018/06/11 Javascript
Vue 样式绑定的实现方法
2019/01/15 Javascript
详解Vue.directive 自定义指令
2019/03/27 Javascript
layui监听工具栏的实例(操作列表按钮)
2019/09/10 Javascript
layui 地区三级联动 form select 渲染的实例
2019/09/27 Javascript
JavaScript如何把两个数组对象合并过程解析
2019/10/10 Javascript
JavaScript 浏览器对象模型BOM原理与常见用法实例分析
2019/12/16 Javascript
swiper实现导航滚动效果
2020/12/13 Javascript
深入理解python try异常处理机制
2016/06/01 Python
python 接口返回的json字符串实例
2018/03/27 Python
Python使用pyodbc访问数据库操作方法详解
2018/07/05 Python
python3中函数参数的四种简单用法
2018/07/09 Python
python适合人工智能的理由和优势
2019/06/28 Python
戴尔加拿大官网:Dell加拿大
2016/09/17 全球购物
高三毕业生自我鉴定
2013/12/20 职场文书
服务员自我评价
2014/01/25 职场文书
法学院方阵解说词
2014/01/29 职场文书
应届毕业生个人求职信范文
2014/01/29 职场文书
八项规定整改措施
2014/02/12 职场文书
中青班党性分析材料
2014/02/16 职场文书
2015年中个人总结范文
2015/03/10 职场文书