Commit 85e66a4e authored by Rich Harris's avatar Rich Harris Committed by Sindre Sorhus

Close GH-655: Ractive.js TodoMVC example app.

parent d2e1c9d8
......@@ -144,6 +144,9 @@
<li class="routing labs">
<a href="labs/architecture-examples/atmajs/" data-source="http://atmajs.com/" data-content="HMVC and the component-based architecture for building client, server or hybrid applications">Atma.js</a>
</li>
<li class="labs">
<a href="labs/architecture-examples/ractive/" data-source="http://ractivejs.org" data-content="Ractive.js is a next-generation DOM manipulation library, optimised for developer sanity.">Ractive.js</a>
</li>
</ul>
<ul class="legend">
<li><b>*</b> <span class="label">R</span> = App also demonstrates routing</li>
......
{
"name": "todomvc-ractive",
"dependencies": {
"todomvc-common": "~0.1.4",
"ractive": "~0.3.5",
"director": "~1.2.0"
}
}
(function () {
'use strict';
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base;
[/labs/, /\w*-examples/].forEach(function (href) {
var match = location.href.match(href);
if (!base && match) {
base = location.href.indexOf(match);
}
});
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').getAttribute('data-framework');
}
if (template && learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.template = template;
this.append();
}
}
Learn.prototype.append = function () {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
});
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
redirect();
getFile('learn.json', Learn);
})();
input[type="checkbox"] {
outline: none;
}
label {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
\ No newline at end of file
<!doctype html>
<html lang="en" data-framework="ractive">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Ractive.js • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section id="todoapp"></section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://rich-harris.co.uk">Rich Harris</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<!-- Templates -->
<script id="main" type="text/ractive">
<header id="header">
<h1>todos</h1>
<!-- In app.js, we define a custom `enter` event, which fires when the user
hits the enter key, submitting a new todo -->
<input id="new-todo" on-enter="newTodo" placeholder="What needs to be done?" autofocus>
</header>
<!-- We only render the main section and the footer if we have one or more todos -
in other words, `items.length` is not falsy -->
{{#items.length}}
<section id="main">
<!-- Here, we compare the total number of tasks (`items.length`) with the number of
completed tasks (`completedTasks().length`). This calculation happens reactively,
so we never need to manually trigger an update. When the `change` event fires
on the input, the `toggleAll` event fires on the Ractive instance. -->
<input id="toggle-all" type="checkbox" on-change="toggleAll" checked='{{ items.length === completedTasks().length }}'>
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
{{#items:i}}
<!-- The {{>item}} partial is defined in the script tag below -->
{{>item}}
{{/items}}
</ul>
</section>
<footer id="footer">
<span id="todo-count">
<!-- The number of active tasks updates reactively -->
<strong>{{ activeTasks().length }}</strong> {{ activeTasks().length === 1 ? 'item' : 'items' }} left
</span>
<ul id="filters">
<li>
<a class="{{ currentFilter === 'all' ? 'selected' : '' }}" href="#/">All</a>
</li>
<li>
<a class="{{ currentFilter === 'active' ? 'selected' : '' }}" href="#/active">Active</a>
</li>
<li>
<a class="{{ currentFilter === 'completed' ? 'selected' : '' }}" href="#/completed">Completed</a>
</li>
</ul>
<!-- This section is only rendered if there are one or more completed tasks -->
{{# completedTasks().length }}
<!-- When the user clicks this button, the `clearCompleted` event fires -->
<button id="clear-completed" on-click="clearCompleted">
Clear completed ({{ completedTasks().length }})
</button>
{{/ end of filter }}
</footer>
{{/items.length}}
</script>
<script id="item" type="text/ractive">
<!-- This is the {{>item}} partial. It is rendered for each task in the array
(`this` corresponds to the current task). But we only want to actually show
those tasks that pass the current filter. -->
{{# filter( this ) }}
<li class="{{ completed ? 'completed' : '' }} {{ editing ? 'editing' : '' }}">
<div class="view">
<input class="toggle" type="checkbox" checked="{{completed}}">
<label on-dblclick="edit">{{description}}</label>
<button on-click="remove:{{i}}" class="destroy"></button>
</div>
<!-- This section only exists in the DOM when editing is taking place -->
{{#.editing}}
<!-- Here, we use custom events (`enter`) alongside standard DOM events
(`blur`) to handle user interaction -->
<input id="edit" class="edit" on-blur-enter="submit" on-escape="cancel" autofocus>
{{/.editing}}
</li>
{{/ end of filter }}
</script>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/ractive/Ractive.js"></script>
<script src="bower_components/director/director.js"></script>
<script src="js/app.js"></script>
<script src="js/persistence.js"></script>
<script src="js/routes.js"></script>
</body>
</html>
/*global window, Ractive */
(function (window, Ractive) {
'use strict';
// Create some filter functions, which we'll need later
var filters = {
completed: function (item) { return item.completed; },
active: function (item) { return !item.completed; }
};
// The keycode for the 'enter' and 'escape' keys
var ENTER_KEY = 13, ESCAPE_KEY = 27;
// Create our Ractive instance
var todoList = new Ractive({
// Specify a target element - an ID, a CSS selector, or the element itself
el: 'todoapp',
// Specify a template, or the ID of a script tag containing the template
template: '#main',
// This is our viewmodel - as well as our list of tasks (which gets added
// later from localStorage - see persistence.js), it includes helper
// functions and computed values
data: {
filter: function (item) {
// Because we're doing `this.get('currentFilter')`, Ractive understands
// that this function needs to be re-executed reactively when the value of
// `currentFilter` changes
var currentFilter = this.get('currentFilter');
if (currentFilter === 'all') {
return true;
}
return filters[currentFilter](item);
},
// completedTasks() and activeTasks() are computed values, that will update
// our app view reactively whenever `items` changes (including changes to
// child properties like `items[1].completed`)
completedTasks: function () {
return this.get('items').filter(filters.completed);
},
activeTasks: function () {
return this.get('items').filter(filters.active);
},
// By default, show all tasks. This value changes when the route changes
// (see routes.js)
currentFilter: 'all'
},
// We can define custom events. Here, we're defining an `enter` event,
// and an `escape` event, which fire when the user hits those keys while
// an input is focused:
//
// <input id="edit" class="edit" on-blur-enter="submit" on-escape="cancel" autofocus>
events: (function () {
var makeCustomEvent = function (keyCode) {
return function (node, fire) {
var keydownHandler = function (event) {
if (event.which === keyCode) {
fire({
node: node,
original: event
});
}
};
node.addEventListener('keydown', keydownHandler, false);
return {
teardown: function () {
node.removeEventListener('keydown', keydownHandler, false);
}
};
};
};
return {
enter: makeCustomEvent(ENTER_KEY),
escape: makeCustomEvent(ESCAPE_KEY)
};
}())
});
// Here, we're defining how to respond to user interactions. Unlike many
// libraries, where you use CSS selectors to dictate what event corresponds
// to what action, in Ractive the 'meaning' of the event is baked into the
// template itself (e.g. <button on-click='remove'>Remove</button>).
todoList.on({
// Removing a todo is as straightforward as splicing it out of the array -
// Ractive intercepts calls to array mutator methods and updates the view
// accordingly. The DOM is updated in the most efficient manner possible.
remove: function (event, index) {
this.get('items').splice(index, 1);
},
// The `event` object contains useful properties for (e.g.) retrieving
// data from the DOM
newTodo: function (event) {
var description = event.node.value.trim();
if (!description) {
return;
}
this.get('items').push({
description: description,
completed: false
});
event.node.value = '';
},
edit: function (event) {
this.set(event.keypath + '.editing', true);
this.nodes.edit.value = event.context.description;
},
submit: function (event) {
var description = event.node.value.trim();
if (!description) {
this.get('items').splice(event.index.i, 1);
return;
}
this.set(event.keypath + '.description', description);
this.set(event.keypath + '.editing', false);
},
cancel: function (event) {
this.set(event.keypath + '.editing', false);
},
clearCompleted: function () {
var items = this.get('items');
var i = items.length;
while (i--) {
if (items[i].completed) {
items.splice(i, 1);
}
}
},
toggleAll: function (event) {
var i = this.get('items').length;
var completed = event.node.checked;
var changeHash = {};
while (i--) {
changeHash['items[' + i + '].completed'] = completed;
}
this.set(changeHash);
}
});
window.todoList = todoList;
})(window, Ractive);
\ No newline at end of file
/*global window, todoList */
(function (window, todoList) {
'use strict';
// In Ractive, 'models' are usually just POJOs - plain old JavaScript objects.
// Our todo list is simply an array of objects, which is handy for fetching
// and persisting from/to localStorage
var items, localStorage, removeEditingState;
// Firefox throws a SecurityError if you try to access localStorage while
// cookies are disabled
try {
localStorage = window.localStorage;
} catch (err) {
todoList.set('items', []);
return;
}
if (localStorage) {
items = JSON.parse(localStorage.getItem('todos-ractive')) || [];
// Editing state should not be persisted, so we remove it
// (https://github.com/tastejs/todomvc/blob/gh-pages/app-spec.md#persistence)
removeEditingState = function (item) {
return {
description: item.description,
completed: item.completed
};
};
// Whenever the model changes (including child properties like
// `items[1].completed`)...
todoList.observe('items', function (items) {
// ...we persist it to localStorage
localStorage.setItem('todos-ractive', JSON.stringify(items.map(removeEditingState)));
});
}
else {
items = [];
}
todoList.set('items', items);
}(window, todoList));
\ No newline at end of file
/*global window, Router, todoList */
(function (window, Router, todoList) {
'use strict';
// We're using https://github.com/flatiron/director for routing
var router = new Router({
'/active': function () {
todoList.set('currentFilter', 'active');
},
'/completed': function () {
todoList.set('currentFilter', 'completed');
}
});
router.configure({
notfound: function () {
window.location.hash = '';
todoList.set('currentFilter', 'all');
}
});
router.init();
}(window, Router, todoList));
\ No newline at end of file
# Ractive.js TodoMVC Example
> Ractive.js solves some of the biggest headaches in web development – data binding, efficient DOM updates, event handling – and does so with almost no learning curve.
> _[Ractive.js - ractivejs.org](http://ractivejs.org)_
## Learning Ractive.js
[Try the 60 second setup](https://github.com/Rich-Harris/Ractive/wiki/60-second-setup), or [follow the interactive tutorials](http://learn.ractivejs.org).
You can find the [API documentation on GitHub](https://github.com/Rich-Harris/Ractive/wiki).
If you have questions, try [Stack Overflow](http://stackoverflow.com/questions/tagged/ractivejs) or [@RactiveJS on Twitter](http://twitter.com/RactiveJS).
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Implementation
Ractive.js isn't an MVC framework in the traditional sense. There are no Model classes, just a plain old array of task objects, and there is only one view object. The app lives in a single file, with two tiny extra files to handle persistence and routing.
This is because Ractive is optimised for developer sanity (as well as performance!). It was initially developed to create interactive news apps at [theguardian.com](http://theguardian.com), which have to be built against punishing deadlines and moving goalposts. Because it embraces reactive programming principles, the developer has less to worry about. Ractive's API is totally straightforward - you can learn it in 5 minutes and master it in a few hours.
It has a number of innovative features: animations, easier event handling, declarative transitions, first-class SVG support, logical expressions in templates with sophisticated dependency tracking. For many developers, it hits the sweet spot between the flexibility of a library like Backbone and the power of a framework like Angular.
## Credit
This TodoMVC application was created by [Rich Harris](http://rich-harris.co.uk).
......@@ -1637,6 +1637,43 @@
}]
}]
},
"ractive": {
"name": "Ractive.js",
"description": "Ractive is a next-generation DOM manipulation library for creating reactive user interfaces, optimised for developer sanity. It was originally developed to create interactive news applications at theguardian.com.",
"homepage": "ractivejs.org",
"examples": [{
"name": "Architecture Example",
"url": "labs/architecture-examples/ractive"
}],
"link_groups": [{
"heading": "Official Resources",
"links": [{
"name": "Ractive.js on GitHub",
"url": "https://github.com/RactiveJS/Ractive"
}, {
"name": "Wiki",
"url": "https://github.com/RactiveJS/Ractive/wiki"
}, {
"name": "60-second setup",
"url": "https://github.com/Rich-Harris/Ractive/wiki/60-second-setup"
}, {
"name": "Interactive tutorials",
"url": "http://learn.ractivejs.org"
}, {
"name": "Examples",
"url": "http://ractivejs.org/examples"
}]
}, {
"heading": "Community",
"links": [{
"name": "Ractive.js on Twitter",
"url": "http://twitter.com/RactiveJS"
}, {
"name": "Ractive.js on Stack Overflow",
"url": "http://stackoverflow.com/questions/tagged/ractivejs"
}]
}]
},
"rappidjs": {
"name": "rAppid.js",
"description": "The declarative Rich Internet Application Javascript MVC Framework.",
......
......@@ -43,6 +43,7 @@ To help solve this problem, we created TodoMVC - a project which offers the same
We also have a number of in-progress applications in Labs:
- [Ractive](http://ractivejs.org)
- [React](http://facebook.github.io/react)
- [Meteor](http://meteor.com)
- [Derby](http://derbyjs.com)
......
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