The todo app from this tutorial is based on http://todomvc.com.
Open Demo in new tab
Installation
1.) Install Xone
Install Xone via NPM (Node Package Manager, requires Node.js):
> npm install -g xone
2.) Create New Xone Project
Create a blank project folder and run from there:
> xone create
Implementation (HTML)
Provide the main index.html (single-page application):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<script src="js/build.js"></script>
</body>
</html>
Define the main app layout:
<section class="todoapp">
{{ include(layout/app/header) }}
{{ include(layout/app/main) }}
{{ include(layout/app/footer) }}
</section>
<footer class="info">
{{ include(layout/app/info) }}
</footer>
Use includes to split views into logically (reusable) components.
The template will be compiled into Javascript by xone compile.
app/layout/app/header.shtml:
app/layout/app/main.shtml:
app/layout/app/footer.shtml:
app/layout/app/info.shtml:
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
app/layout/app/main.shtml:
<section class="main">
<input class="toggle-all" id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
</section>
app/layout/app/footer.shtml:
<footer class="footer">
<span class="todo-count"><strong>0</strong> item left</span>
<ul class="filters">
<li><a href="#/" class="selected">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
app/layout/app/info.shtml:
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://todomvc.com">Xone</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
Define the todo list view (as a dynamic template):
<li {{ if(completed) }}class="completed"{{ endif }} data-id="{{ id }}">
<div class="view">
<input class="toggle" type="checkbox" {{ if(completed) }}checked{{ endif }}>
<label class="title">{{ title }}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{ title }}">
</li>
The template will be compiled into Javascript by xone compile.
The markups {{ ... }} will be resolved during runtime by Controller.render() or Controller.build().
The template represents the view for one single item and is automatically multiplied by passing an array of item data.
Implementation (Javascript)
Define a persistent todo model:
goog.provide('APP.MODEL.Todo');
goog.require('APP.MODEL');
/**
* @type _model_helper
*/
APP.MODEL.Todo = (function(){
var Todo = APP.MODEL.register('Todo', [
'id', 'title', 'completed'
]);
// model events (callbacks):
Todo.onCreate =
Todo.onUpdate =
Todo.onDelete = function(){
APP.CONTROLLER.Main();
};
return Todo;
})();
Implement all actions by providing an application interface (e.g. used as event handlers):
goog.provide('APP.HANDLER.Event');
goog.require('APP.MODEL.Todo');
APP.HANDLER = (function(Todo){
return {
createTodo: function(event, target){
var value = CORE.trim(this.value);
if(value) Todo.create({
'id': CORE.randomString(8),
'title': value,
'completed': false
});
},
editTodo: function(event, target){
var value = CORE.trim(this.value);
if(value){
Todo.update(target, 'title', value, /* save? */ true);
}
else{
Todo.delete(target);
}
},
cancelCreate: function(event, target){
this.value = '';
},
cancelEdit: function(event, target){
this.value = CORE.getClosest(target, '>.title').textContent;
CORE.removeClass(target, 'editing');
},
updateState: function(event, target){
Todo.find(target)
.update('completed', this.checked)
.save();
},
toggleAllStates: function(event, target){
Todo.updateAll('completed', this.checked, /* save? */ true);
},
deleteCompleted: function(event, target){
Todo.deleteWhere('completed', true);
},
deleteTodo: function(event, target){
Todo.delete(target);
},
enterEditMode: function(event, target){
CORE.addClass(target, 'editing');
CORE.focusInput(CORE.getClosest(target, '>.edit'));
}
};
})(APP.MODEL.Todo);
Define event delegation and route to handler:
goog.provide('APP.EVENT.App');
goog.require('APP.EVENT');
goog.require('APP.HANDLER.Event');
(function(HANDLER){
APP.EVENT['document'] = [{
on: 'keyup:enter',
if: 'input.new-todo',
do: [HANDLER.createTodo, HANDLER.cancelCreate]
}, {
on: 'keyup:esc',
if: 'input.new-todo',
do: HANDLER.cancelCreate
}, {
on: ['keyup:enter', 'focusout'],
if: 'input.edit',
at: '< li',
do: [HANDLER.editTodo, HANDLER.cancelEdit]
}, {
on: 'keyup:esc',
if: 'input.edit',
at: '< li',
do: HANDLER.cancelEdit
}, {
on: 'change',
if: 'input.toggle',
at: '< li',
do: HANDLER.updateState
}, {
on: 'clickmove',
if: 'button.destroy',
at: '< li',
do: HANDLER.deleteTodo
}, {
on: 'dblclick',
if: 'label.title',
at: '< li',
do: HANDLER.enterEditMode
}, {
on: 'clickmove',
if: 'button.clear-completed',
do: HANDLER.deleteCompleted
}, {
on: 'change',
if: 'input.toggle-all',
do: HANDLER.toggleAllStates
}];
})(APP.HANDLER);
Provide a main controller (e.g. a view controller):
goog.provide('APP.CONTROLLER.Main');
goog.require('APP.CONTROLLER');
goog.require('APP.MODEL.Todo');
APP.CONTROLLER.Main = (function(Todo){
var current_filter;
return function MainController(params, target){
// update select state:
if(target){
current_filter = params;
CORE.toggleClass(['a.selected', target], 'selected');
}
// render view:
APP.CONTROLLER.render({
// uses view "app/view/todo/list.shtml"
view: 'view/todo/list',
// models to render
data: filterTodosBy(current_filter),
// destination dom element
target: 'ul.todo-list',
// callback
callback: updateView
});
};
// private helpers:
function filterTodosBy(filter){
return filter ?
Todo.where('completed', filter === 'completed')
:
Todo.all();
}
function updateView(){
var count_all = Todo.count();
var count_active = Todo.count('completed', false);
var count_completed = count_all - count_active;
// update container visibility
CORE.setStyle(['.main', '.footer'], 'display',
count_all ? 'block' : 'none'
);
// update button visibility
CORE.setStyle('.clear-completed', 'display',
count_completed ? 'block' : 'none'
);
// update counter
CORE.setHTML('.todo-count',
'<strong>' + count_active + '</strong> item' + (count_active === 1 ? '' : 's') + ' left'
);
// update checkbox
CORE.getById('toggle-all').checked = !count_active;
}
})(APP.MODEL.Todo);
The controller is connected/mapped by route and event definitions. When using the controller as a view controller it typically also renders views by data.
Define routing:
goog.provide('APP.ROUTE.App');
goog.require('APP.CONTROLLER.Main');
APP.ROUTE = {
'#/': APP.CONTROLLER.Main,
'#/active': {
to: APP.CONTROLLER.Main,
params: 'active'
},
'#/completed': {
to: APP.CONTROLLER.Main,
params: 'completed'
}
};
Provide a main class (entry point):
goog.provide('APP.MAIN');
goog.require('APP.EVENT.App');
goog.require('APP.ROUTE.App');
// define app layout:
APP.CONFIG.LAYOUT = [
'layout/app' // points to "app/layout/app.shtml"
];
// provide entry point:
APP.MAIN = function(){
APP.CONTROLLER.request(window.location.hash);
};
Define all app dependencies in app/manifest.js:
var MANIFEST = {
"dependencies": {
"calculate": true,
"xone": "lib/xone/",
"copy": [
"index.html"
],
"css": [
"node_modules/todomvc-common/base.css",
"node_modules/todomvc-app-css/index.css"
],
"js_extern": [
"node_modules/todomvc-common/base.js"
],
"js": []
}
};
Build & Run
1.) Build Xone Project
Compile the project:
> xone compile
Build the project (also compiles):
> xone build
2.) Run Xone Project
Run app/index.html or public/www/index.html from your local filesystem in your preferred browser.
Start local webserver:
> xone server
Open your browser and goto:
- http://localhost/app/ to run development environment
- http://localhost/public/www/ to run production build (www is the default platform)