Building a todo app with the Xone Javascript framework
 
 Start Tutorial 
 
 


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:
                    <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: