Commit 8222e002 authored by Michael Berkompas's avatar Michael Berkompas

Added Lab for KnockoutJS ClassBindingProvider

Implements Ryan Niemeyer's classBindingProvider in the default
KnockoutJS TodoMVC app.
parent 7ffb0a02
......@@ -220,6 +220,9 @@
<li>
<a href="labs/architecture-examples/o_O/" data-source="http://weepy.github.com/o_O/" data-content="o_O: HTML binding for teh lulz: &lt;br> - Elegantly binds objects to HTML&lt;br>- Proxies through jQuery, Ender, etc&lt;br>- Automatic dependency resolution&lt;br>- Plays well with others">Funnyface.js</a>
</li>
<li>
<a href="labs/architecture-examples/knockoutjs_classBindingProvider/" data-source="https://github.com/rniemeyer/knockout-classBindingProvider" data-content="This project is an adaptation of /architecture-examples/knockoutjs with Ryan Niemeyer's Class Binding Provider.">Knockout + ClassBindingProvider</a>
</li>
<li>
<a href="labs/dependency-examples/knockoutjs_require/" data-source="http://knockoutjs.com" data-content="This project is an adaptation of /architecture-examples/knockoutjs with require.js.">Knockout + RequireJS</a>
</li>
......
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<title>Knockout.js • TodoMVC</title>
<link href="../../../assets/base.css" rel="stylesheet">
<!--[if IE]>
<script src="../../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" data-class="todos.new" placeholder="What needs to be done?" autofocus>
</header>
<section id="main" data-class="todos.listVisible">
<input id="toggle-all" data-class="todos.allCompleted" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-class="todos.foreach">
<li data-class="todos.todo.item">
<div class="view">
<input class="toggle" data-class="todos.todo.completed" type="checkbox">
<label data-class="todos.todo.readOnlyValue"></label>
<button class="destroy" data-class="todos.todo.destroy"></button>
</div>
<input class="edit" data-class="todos.todo.editingValue">
</li>
</ul>
</section>
<footer id="footer" data-class="footer.isVisible">
<span id="todo-count">
<strong data-class="footer.remainingCount">0</strong>
<span data-class="footer.remainingCountText"></span> left
</span>
<ul id="filters">
<li>
<a data-class="footer.filters.all" href="#/all">All</a>
</li>
<li>
<a data-class="footer.filters.active" href="#/active">Active</a>
</li>
<li>
<a data-class="footer.filters.completed" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-class="footer.clearCompleted">
Clear completed (<span data-class="footer.completedCount"></span>)
</button>
</footer>
</section>
<footer id="info">
<span data-class="todos.listVisible">Double-click to edit a todo</span>
<p>Original Knockout version from <a href="https://github.com/ashish01/knockoutjs-todos" target="_blank">Ashish Sharma</a></p>
<p>Rewritten to use Knockout 2.0 and standard template by <a href="http://knockmeout.net" target="_blank">Ryan Niemeyer</a></p>
<p>Patches/fixes for cross-browser compat: <a href="http://twitter.com/addyosmani" target="_blank">Addy Osmani</a></p>
<p>Implemented Knockout classBindingProvider: <a href="http://github.com/mberkom" target="_blank">Michael Berkompas</a></p>
</footer>
<script src="../../../assets/base.js"></script>
<script src="../../../assets/director.min.js"></script>
<script src="js/lib/knockout.min.js"></script>
<script src="js/lib/knockout-classBindingProvider.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
/*global ko, crossroads */
(function() {
'use strict';
var ENTER_KEY = 13;
// a custom binding to handle the enter key (could go in a separate library)
ko.bindingHandlers.enterKey = {
init: function( element, valueAccessor, allBindingsAccessor, data ) {
var wrappedHandler, newValueAccessor;
// wrap the handler with a check for the enter key
wrappedHandler = function( data, event ) {
if ( event.keyCode === ENTER_KEY ) {
valueAccessor().call( this, data, event );
}
};
// create a valueAccessor with the options that we would want to pass to the event binding
newValueAccessor = function() {
return {
keyup: wrappedHandler
};
};
// call the real event binding's init function
ko.bindingHandlers.event.init( element, newValueAccessor, allBindingsAccessor, data );
}
};
// wrapper to hasfocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
init: function( element, valueAccessor, allBindingsAccessor ) {
ko.bindingHandlers.hasfocus.init( element, valueAccessor, allBindingsAccessor );
ko.utils.registerEventHandler( element, 'focus', function() {
element.focus();
});
},
update: function( element, valueAccessor ) {
ko.utils.unwrapObservable( valueAccessor() ); // for dependency
// ensure that element is visible before trying to focus
setTimeout(function() {
ko.bindingHandlers.hasfocus.update( element, valueAccessor );
}, 0);
}
};
// represent a single todo item
var Todo = function( title, completed ) {
this.title = ko.observable( title );
this.completed = ko.observable( completed );
this.editing = ko.observable( false );
};
// our main view model
var ViewModel = function( todos ) {
var self = this;
// map array of passed in todos to an observableArray of Todo objects
self.todos = ko.observableArray(ko.utils.arrayMap( todos, function( todo ) {
return new Todo( todo.title, todo.completed );
}));
// store the new todo value being entered
self.current = ko.observable();
self.showMode = ko.observable('all');
self.filteredTodos = ko.computed(function() {
switch( self.showMode() ) {
case 'active':
return self.todos().filter(function( todo ) {
return !todo.completed();
});
case 'completed':
return self.todos().filter(function( todo ) {
return todo.completed();
});
default:
return self.todos();
}
});
// add a new todo, when enter key is pressed
self.add = function() {
var current = self.current().trim();
if ( current ) {
self.todos.push( new Todo( current ) );
self.current('');
}
};
// remove a single todo
self.remove = function( todo ) {
self.todos.remove( todo );
};
// remove all completed todos
self.removeCompleted = function() {
self.todos.remove(function( todo ) {
return todo.completed();
});
};
// edit an item
self.editItem = function( item ) {
item.editing( true );
};
// stop editing an item. Remove the item, if it is now empty
self.stopEditing = function( item ) {
item.editing( false );
if ( !item.title().trim() ) {
self.remove( item );
}
};
// count of all completed todos
self.completedCount = ko.computed(function() {
return ko.utils.arrayFilter( self.todos(), function( todo ) {
return todo.completed();
}).length;
});
// count of todos that are not complete
self.remainingCount = ko.computed(function() {
return self.todos().length - self.completedCount();
});
// writeable computed observable to handle marking all complete/incomplete
self.allCompleted = ko.computed({
//always return true/false based on the done flag of all todos
read: function() {
return !self.remainingCount();
},
// set all todos to the written value (true/false)
write: function(newValue) {
ko.utils.arrayForEach( self.todos(), function( todo ) {
// set even if value is the same, as subscribers are not notified in that case
todo.completed( newValue );
});
}
});
// helper function to keep expressions out of markup
self.getLabel = function( count ) {
return ko.utils.unwrapObservable( count ) === 1 ? 'item' : 'items';
};
// internal computed observable that fires whenever anything changes in our todos
ko.computed(function() {
// store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each item
localStorage.setItem('todos-knockout', ko.toJSON( self.todos ) );
}).extend({
throttle: 500
}); // save at most twice per second
};
// check local storage for todos
var todos = ko.utils.parseJson( localStorage.getItem('todos-knockout') );
// setup bindings and classBindingProvider
var bindings = {
todos: {
'new': function() {
return {
value: this.current,
valueUpdate: 'afterkeydown',
enterKey: this.add
};
},
'listVisible': function() {
return {
visible: this.todos().length
};
},
'allCompleted': function() {
return {
checked: this.allCompleted
};
},
'foreach': function() {
return {
foreach: this.filteredTodos
};
},
todo: {
'item': function() {
return {
css: {
completed: this.completed,
editing: this.editing
}
};
},
'completed': function () {
return {
checked: this.completed
};
},
'readOnlyValue': function(context) {
return {
text: this.title,
'event': {
dblclick: context.$root.editItem
}
}
},
'editingValue': function(context) {
return {
value: this.title,
valueUpdate: 'afterkeydown',
enterKey: context.$root.stopEditing,
selectAndFocus: this.editing,
event: {
blur: context.$root.stopEditing
}
};
},
'destroy': function (context) {
return {
click: context.$root.remove
};
}
}
},
footer: {
'isVisible': function () {
return {
visible: this.completedCount() || this.remainingCount()
};
},
'remainingCount': function () {
return {
text: this.remainingCount
};
},
'remainingCountText': function() {
return {
text: this.getLabel( this.remainingCount )
};
},
filters: {
'all': function() {
return {
css: { selected: this.showMode() === 'all' }
};
},
'active': function() {
return {
css: { selected: this.showMode() === 'active' }
};
},
'completed': function() {
return {
css: { selected: this.showMode() === 'completed' }
};
}
},
'clearCompleted': function() {
return {
visible: this.completedCount,
click: this.removeCompleted
};
},
'completedCount': function() {
return {
text: this.completedCount()
};
}
}
};
ko.bindingProvider.instance = new ko.classBindingProvider(bindings);
// bind a new instance of our view model to the page
var viewModel = new ViewModel( todos || [] );
ko.applyBindings( viewModel );
// set up filter routing
Router({ '/:filter': viewModel.showMode }).init();
}());
//knockout-classBindingProvider v0.4.0 | (c) 2012 Ryan Niemeyer | http://www.opensource.org/licenses/mit-license
!function(e){typeof define=="function"&&define.amd?define(["knockout","exports"],e):e(ko)}(function(e,t,n){var r=function(t,n){var r=new e.bindingProvider;n=n||{},this.attribute=n.attribute||"data-class",this.virtualAttribute="ko "+(n.virtualAttribute||"class")+":",this.fallback=n.fallback,this.bindings=t||{},this.bindingRouter=n.bindingRouter||function(e,t){var n,r,i,s;if(t[e])return t[e];i=e.split("."),s=t;for(var n=0,r=i.length;n<r;n++)s=s[i[n]];return s},this.registerBindings=function(t){e.utils.extend(this.bindings,t)},this.nodeHasBindings=function(e){var t,n;return e.nodeType===1?t=e.getAttribute(this.attribute):e.nodeType===8&&(n=""+e.nodeValue||e.text,t=n.indexOf(this.virtualAttribute)>-1),!t&&this.fallback&&(t=r.nodeHasBindings(e)),t},this.getBindings=function(t,n){var i,s,o,u,a={},f,l,c="";t.nodeType===1?c=t.getAttribute(this.attribute):t.nodeType===8&&(f=""+t.nodeValue||t.text,l=f.indexOf(this.virtualAttribute),l>-1&&(c=f.substring(l+this.virtualAttribute.length)));if(c){c=c.replace(/^(\s|\u00A0)+|(\s|\u00A0)+$/g,"").replace(/(\s|\u00A0){2,}/g," ").split(" ");for(i=0,s=c.length;i<s;i++)o=this.bindingRouter(c[i],this.bindings),o&&(u=typeof o=="function"?o.call(n.$data,n,c):o,e.utils.extend(a,u))}else this.fallback&&(a=r.getBindings(t,n));return a}};return t||(e.classBindingProvider=r),r})
\ No newline at end of file
# Knockout.js TodoMVC app with the Class Binding Provider
The Class Binding Provider offers advanced KnockoutJS developers a modular and reusable way to write their bindings. It works similarly to css classes providing you with a way to declare your bindings in JavaScript and then reference them using keys in your HTML.
To learn how to implement the Class Binding Provider, read the docs [here](https://github.com/rniemeyer/knockout-classBindingProvider).
**What are the benefits of this approach to Knockout bindings?**
* The markup can stay clean and simple
* Bindings can be re-used, even at different scopes
* You can set breakpoints in the bindings to inspect the data being passed through them
* You can do logging in the bindings to understanding how many times they are being called
* You can change/alter the bindings on an element whenever your bindings are triggered
* Bindings go through less parsing (do not need to go from a object literal in a string to code)
**Contributions**
[ashish101](https://github.com/ashish01/knockoutjs-todos) wrote the original version of this application, which was then refactored by Addy Osmani and later rewritten by TodoMVC contributors.
[mberkom](https://github.com/mberkom) rewrote the binding references to use Ryan Niemeyer's Class Binding Provider instead of Knockout's default `data-bind` method of binding.
\ No newline at end of file
......@@ -61,6 +61,7 @@ We also have a number of in-progress applications in Labs:
- [Dijon](https://github.com/creynders/dijon-framework)
- [rAppid.js](http://www.rappidjs.com)
- [o_O](http://weepy.github.com/o_O)
- [KnockoutJS](http://knockoutjs.com) + [ClassBindingProvider](https://github.com/rniemeyer/knockout-classBindingProvider) (using Ryan Niemeyer's Class Binding Provider)
- [KnockoutJS](http://knockoutjs.com) + [RequireJS](http://requirejs.org) (using AMD)
- [AngularJS](http://angularjs.org) + [RequireJS](http://requirejs.org) (using AMD)
- [AngularJS](http://angularjs.org) (optimized)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment