使用到Backbone.View、Backbone.Model、Backbone.Collection、Backbone.history和Backbone.Router相关的知识点。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
ul,
li {
list-style: none;
padding: 0;
margin: 0;
}
.box {
width: 550px;
margin: 20px auto;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 20%), 0 25px 50px 0 rgb(0 0 0 / 10%);
position: relative;
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.box header {
padding: 10px 20px 10px 60px;
border-bottom: 1px solid #ddd;
}
.box .new-todo {
border: none;
width: 100%;
font-size: 24px;
}
.box .new-todo:focus {
outline: none;
}
input::placeholder {
color: #ddd;
}
.box .toggle-all {
opacity: 0;
position: absolute;
z-index: 2;
}
.box .toggle-all+label {
transform: rotate(90deg);
display: inline-block;
width: 45px;
height: 45px;
position: absolute;
top: 0;
left: 0;
text-align: center;
line-height: 45px;
font-size: 28px;
color: #ddd;
user-select: none;
cursor: pointer;
}
.box .toggle-all:checked+label {
color: #333;
}
.box .todo-list li {
border-bottom: 1px solid #ddd;
position: relative;
}
.box .todo-list .view {
position: relative;
}
.box .todo-list .view .toggle {
position: absolute;
left: 8px;
bottom: 11px;
width: 23px;
height: 23px;
opacity: 0;
cursor: pointer;
}
.box .todo-list .view label {
display: block;
padding: 10px 50px;
font-size: 20px;
color: #555;
background: #fff url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E') no-repeat left 2px;
}
.box .todo-list .view .toggle:checked+label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.box .todo-list .destroy {
background-color: #fff;
color: red;
border: 0;
position: absolute;
right: 10px;
top: 7px;
font-size: 26px;
cursor: pointer;
display: none;
}
.box .todo-list .view label:hover+.destroy {
display: block;
}
.box .todo-list .view .destroy:hover {
display: block;
}
.box .todo-list .edit {
display: none;
padding: 10px;
font-size: 20px;
margin-left: 43px;
width: 482px;
}
.box .todo-list .editing .edit {
display: block;
}
.box .todo-list .editing .view {
display: none;
}
.box .todo-list .completed .view label {
text-decoration: line-through;
color: #ddd;
}
.box footer {
font-size: 12px;
color: #666;
padding: 15px 20px;
position: relative;
line-height: 12px;
}
.box footer::before {
position: absolute;
content: ' ';
height: 50px;
display: block;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 1px 1px #ddd, 0 8px 0 -3px #fff, 0 9px 1px -3px #ddd, 0 16px 0 -6px #fff, 0 17px 1px -6px #ddd;
}
.box footer a {
color: #666;
text-decoration: none;
}
.box footer .filters {
position: absolute;
top: 10px;
left: 0;
right: 0;
text-align: center;
}
.box footer .filters li {
display: inline-block;
margin-right: 20px;
padding: 5px 8px;
border-radius: 2px;
line-height: 12px;
}
.box footer .clear-completed {
float: right;
border: 0;
color: #666;
cursor: pointer;
background-color: #fff;
position: relative;
}
.box footer .filters li.selected {
border: 1px solid #b53636;
}
.hide {
display: none;
}
</style>
</head>
<body>
<div class="box">
<header>
<input type="text" class="new-todo" placeholder="What needs tobe done">
</header>
<section class="hide">
<input type="checkbox" id="toggle-all" class="toggle-all">
<label for="toggle-all">❯</label>
<ul class="todo-list"></ul>
</section>
<footer class="hide">
</footer>
</div>
<script type="text/template" id="todo-item">
<div class="view">
<input type="checkbox" class="toggle" <%= completed ? 'checked' : '' %>>
<label><%= title %></label>
<button type="button" class="destroy">x</button>
</div>
<input type="text" class="edit" value="<%= title %>">
</script>
<script type="text/template" id="start-template">
<span class="todo-count">
<strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left
</span>
<ul class="filters">
<li>
<a href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<% if(completed) { %>
<button class="clear-completed">Clear completed</button>
<% } %>
</script>
<script src="./../js/jquery-3.4.1.min.js"></script>
<script src="./../js/underscore-min.js"></script>
<script src="./../js/backbone.js"></script>
<script>
var Todo = Backbone.Model.extend({
defaults: {
title: '',
completed: false
}
});
var Todos = Backbone.Collection.extend({
model: Todo,
completed: function () {
return this.where({ completed: true })
},
remaining: function () {
return this.where({ completed: false })
}
});
var AppView = Backbone.View.extend({
el: '.box',
template: _.template($('#start-template').html()),
initialize: function () {
this.render();
this.allCheckbox = this.$('#toggle-all')[0];
this.listenTo(todos, 'add', this.infoBoxShow);
this.listenTo(todos, 'destroy', this.infoBoxShow);
//监听所有todo的completed属性变化
this.listenTo(todos, 'change:completed', this.changeCompleted);
//监听底部按钮
this.on('filter', this.filter);
},
render: function () {
this.$('footer').html(this.template({
remaining: todos.remaining().length,
completed: todos.completed().length
}));
},
events: {
'keyup .new-todo': 'addTodo',
'click .toggle-all': 'allSelected',
'click .clear-completed': 'clearCompleted'
},
addTodo: function (event) {
if (event.keyCode !== 13) {
return false;
}
var $newTodo = this.$el.find('.new-todo');
var val = $newTodo.val();
if (!$.trim(val)) {
return false;
}
var todo = new Todo({
title: val
});
todos.add(todo);
var todoView = new TodoView({
model: todo
});
var ele = todoView.render().$el;
if (globalFilter === 'completed') {
ele.addClass('hide');
}
this.$el.find('.todo-list').append(ele);
$newTodo.val('');
},
allSelected: function () {
var completed = this.allCheckbox.checked;
todos.each(function (model) {
model.set('completed', completed)
});
},
infoBoxShow() { //判断section DOM是否显示
if (todos.length) {
this.$('section').removeClass('hide');
this.$('footer').removeClass('hide');
} else {
this.$('section').addClass('hide');
this.$('footer').addClass('hide');
}
this.render();
this.footerAddAction();
},
clearCompleted() {
_.invoke(todos.completed(), 'destroy');
},
changeCompleted: function (model) {
//全选按钮
$(this.allCheckbox).prop('checked', todos.completed().length);
this.render();
this.filter();
},
footerAddAction: function () {
this.$('.filters li').each(function () {
$(this).removeClass('selected');
var href = $(this).find('a[href="#/' + (globalFilter ? globalFilter : '') + '"]').closest('li').addClass('selected');
});
},
filter: function () {
todos.each(function (model) {
model.trigger('filter')
});
this.footerAddAction();
}
});
var TodoView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#todo-item').html()),
initialize: function () {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'filter', this.toggleVisible)
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
if (this.model.get('completed')) {
this.$el.addClass('completed');
} else {
this.$el.removeClass('completed');
}
return this;
},
events: {
'click input.toggle': 'toogle',
'click button.destroy': 'destroy',
'dblclick label': 'showUpdateBox',
'blur .edit': 'updeteTitle',
},
toogle: function (e) {
this.model.set('completed', $(e.currentTarget).is(':checked'));
this.toggleVisible();
},
destroy: function (e) {
this.model.destroy();//从todos中移除model
this.remove(); //删除view
},
showUpdateBox: function () {
var $input = this.$('.edit');
var textLength = $input.val().length;
this.$el.addClass('editing')
$input.focus();
$input[0].setSelectionRange(textLength, textLength);
},
updeteTitle: function () {
var val = this.$('.edit').val();
if (!val) {
this.destroy();
} else {
this.model.set('title', val);
}
this.$el.removeClass('editing')
},
toggleVisible: function () {
this.$el.toggleClass('hide', this.isHidden());
},
isHidden: function () {
return this.model.get('completed') ? globalFilter === 'active' : globalFilter === 'completed';
}
});
var TodoRouter = Backbone.Router.extend({
routes: {
'*filter': 'filter',
},
filter: function (params) {
globalFilter = params;
appview.trigger('filter');
}
});
var todos = new Todos();
var appview = new AppView();
var todoRouter = new TodoRouter();
Backbone.history.start();
</script>
</body>
</html>