Commit f8d5c28a authored by Brian Kotek's avatar Brian Kotek

ExtJS with DeftJS version of TodoMVC.

parent 6e43c8dd
build
spec
coffee
tools
css/base.css
css/bg.png
# ExtJS with DeftJS TodoMVC Example
This is an example of the TodoMVC application using [Sencha's ExtJS](http://www.sencha.com/) enhanced with the [DeftJS library](http://deftjs.org/). DeftJS is an MVC framework and inversion of control (IoC) container, and provides many other features such as a Promises API, live event listeners, etc.
Instead of the singleton-based, stateless controllers promoted by Sencha's built-in MVC system, DeftJS uses ViewControllers which are dynamically attached to each view. This is essentially an implementation of the [Passive View pattern](http://martinfowler.com/eaaDev/PassiveScreen.html), similar to Cocoa's UIViewControllers. The source code and full documentation can be found via the [DeftJS home page](http://deftjs.org/).
Because the application is quite small, the benefits of ViewControllers and IoC aren't very obvious. The advantages become pronounced on larger applications.
It's also worth noting that the TodoMVC application is **not** very typical of an ExtJS-based application. ExtJS is more applicable to large-scale [rich internet application (RIA)](http://en.wikipedia.org/wiki/Rich_Internet_application) development. The XTemplate approach used here to more easily leverage the mandated CSS and layout is not a very common sight.
ExtJS has a very advanced layout and component system, along with full-blown SASS-based themes. So while this example looks and functions exactly as the TodoMVC App Specification mandates, this isn't really how views in a "normal" ExtJS or Sencha Touch application would be set up. Sencha has [many examples of the available components and layouts](http://docs.sencha.com/ext-js/4-1/#!/example), if you'd like to see the more typical options.
\ No newline at end of file
/* Add your own styles here. */
.mousepointer { cursor: pointer; }
*:focus {
outline: none;
}
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ExtJS with DeftJS • TodoMVC</title>
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="../../../assets/base.css" />
<link rel="stylesheet" type="text/css" href="css/app.css" />
<!-- Note that the ie.js from the assets folder is omitted because ExtJS automatically handles all cross-browser logic. -->
<!-- ExtJS -->
<script type="text/javascript" src="lib/extjs/ext-all.js"></script>
<!-- Application -->
<script type="text/javascript" src="js/app-loader.js"></script>
<script type="text/javascript" src="lib/deft/deft.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</head>
<body></body>
</html>
\ No newline at end of file
'use strict';
/**
* DeftJS Application class for the TodoDeftJS application.
*/
Ext.define('TodoDeftJS.Application', {
extend: 'Deft.mvc.Application',
requires: ['TodoDeftJS.util.TemplateLoader', 'TodoDeftJS.store.TodoStore', 'TodoDeftJS.view.Viewport'],
/**
* init() runs when Ext.onReady() is called.
*/
init: function () {
this.beforeInit();
Deft.Injector.configure(this.buildInjectorConfiguration());
return this.afterInit();
},
/**
* @protected
* Returns the configuration object to pass to Deft.Injector.configure(). Override in subclasses to alter the Injector configuration before returning the config object.
* @return {Object} The Injector configuration object.
*/
buildInjectorConfiguration: function () {
var config;
config = {
templateLoader: 'TodoDeftJS.util.TemplateLoader',
todoStore: 'TodoDeftJS.store.TodoStore'
};
return config;
},
/**
* @protected
* Runs at the start of the init() method. Override in subclasses if needed.
*/
beforeInit: function () {},
/**
* @protected
* Runs at the end of the init() method. Useful to create initial Viewport, start Jasmine tests, etc.
*/
afterInit: function () {
Ext.tip.QuickTipManager.init();
return Ext.create('TodoDeftJS.view.Viewport');
}
});
\ No newline at end of file
Ext.Loader.setConfig({
enabled: true,
paths: {
'TodoDeftJS': 'js'
}
});
Ext.syncRequire(['Ext.Component', 'Ext.ComponentManager', 'Ext.ComponentQuery']);
\ No newline at end of file
Ext.syncRequire(['TodoDeftJS.Application']);
Ext.create('TodoDeftJS.Application');
\ No newline at end of file
/**
* Controls the main (root) UI container for the application.
*/
Ext.define('TodoDeftJS.controller.TodoController', {
extend: 'Deft.mvc.ViewController',
inject: ['todoStore'],
config: {
todoStore: null,
currentTodo: null
},
control: {
view: {
beforecontainerkeydown: 'onNewTodoKeyDown',
beforecontainerclick: 'onTodoToolsClick',
beforeitemclick: 'onTodoClick',
beforeitemdblclick: 'onTodoEditClick',
beforeitemkeydown: 'onEditTodoKeyDown'
}
},
init: function () {
this.callParent(arguments);
return this;
},
addTodo: function (title) {
var newTodo;
title = title.trim();
if(title.length) {
newTodo = Ext.create('TodoDeftJS.model.Todo', {
title: Ext.util.Format.htmlEncode(title),
completed: false
});
this.getTodoStore().add(newTodo);
}
Ext.dom.Query.selectNode('#new-todo').focus();
},
toggleCompleted: function (todo) {
todo.set('completed', !todo.get('completed'));
},
deleteTodo: function (todo) {
this.getTodoStore().remove(todo);
},
updateTodo: function (todo, title) {
this.setCurrentTodo(null);
if ((todo === null) || (todo === undefined)) {
return;
}
todo.set('editing', false);
title = title.trim();
if (((title != null) && (title != undefined) && title.length)) {
todo.set('title', Ext.util.Format.htmlEncode(title));
} else {
this.deleteTodo(todo);
}
Ext.dom.Query.selectNode('#new-todo').focus();
},
completedCount: function () {
return this.getTodoStore().completedCount();
},
incompleteCount: function () {
return this.getTodoStore().incompleteCount();
},
areAllComplete: function () {
return this.getTodoStore().completedCount() === this.getTodoStore().count();
},
onNewTodoKeyDown: function (view, event) {
var title;
if (event.target.id === 'new-todo' && event.keyCode === Ext.EventObject.ENTER) {
title = event.target.value.trim();
this.addTodo(title);
event.target.value = null;
return false;
}
return true;
},
onTodoEditClick: function (view, todo, item, idx, event) {
var editField;
this.setCurrentTodo(todo);
todo.set('editing', true);
editField = Ext.dom.Query.selectNode('#todo-list li.editing .edit');
editField.focus();
// Ensure that focus() doesn't select all the text as well by resetting the value.
editField.value = editField.value;
Ext.fly(editField).on('blur', this.onTodoBlur, this);
return false;
},
onTodoBlur: function (event, target) {
Ext.fly(event.target).un('blur', this.onTodoBlur, this);
if ((target != null) && (target != undefined)) {
return this.updateTodo(this.getCurrentTodo(), target.value.trim());
}
},
onEditTodoKeyDown: function (view, todo, item, idx, event) {
var title;
if (event.keyCode === Ext.EventObject.ENTER) {
if (event.target.id === 'new-todo') {
this.onNewTodoKeyDown(view, event);
return false;
}
title = event.target.value.trim();
Ext.fly(event.target).un('blur', this.onTodoBlur, this);
this.updateTodo(todo, title);
return false;
}
return true;
},
onTodoClick: function (view, todo, item, idx, event) {
if (Ext.fly(event.target).hasCls('toggle')) {
this.toggleCompleted(todo);
} else if (Ext.fly(event.target).hasCls('destroy')) {
this.deleteTodo(todo);
}
return true;
},
onTodoToolsClick: function (view, event) {
if (Ext.fly(event.target).hasCls('toggleall')) {
this.getTodoStore().toggleAllCompleted(event.target.checked);
} else if (Ext.fly(event.target).hasCls('clearcompleted')) {
this.getTodoStore().deleteCompleted();
}
return true;
}
});
\ No newline at end of file
/**
* Models a To-Do.
*/
Ext.define('TodoDeftJS.model.Todo', {
extend: 'Ext.data.Model',
fields: [
{
name: 'id'
}, {
name: 'title',
type: 'string'
}, {
name: 'completed',
type: 'boolean'
}, {
name: 'editing',
type: 'boolean',
defaultValue: false,
persist: false
}
],
constructor: function () {
this.callParent(arguments);
return this;
}
});
\ No newline at end of file
/**
* A persistent collection of To-Do models.
*/
Ext.define('TodoDeftJS.store.TodoStore', {
extend: 'Ext.data.Store',
requires: ['TodoDeftJS.model.Todo'],
model: 'TodoDeftJS.model.Todo',
autoLoad: true,
autoSync: true,
proxy: {
type: 'localstorage',
id: 'todos-deftjs'
},
completedCount: function () {
var numberComplete;
numberComplete = 0;
this.each(function (todo) {
if (todo.get('completed')) {
return numberComplete++;
}
});
return numberComplete;
},
incompleteCount: function () {
var numberInomplete;
numberInomplete = 0;
this.each(function (todo) {
if (!todo.get('completed')) {
return numberInomplete++;
}
});
return numberInomplete;
},
findEditingTodo: function () {
var editingTodo;
editingTodo = null;
this.each(function (todo) {
if (todo.get('editing')) {
editingTodo = todo;
return false;
}
});
return editingTodo;
},
toggleAllCompleted: function (isCompleted) {
this.each(function (todo) {
return todo.set('completed', isCompleted);
});
},
deleteCompleted: function () {
var removedTodos;
removedTodos = [];
this.each(function (todo) {
if (todo.get('completed')) {
return removedTodos.push(todo);
}
});
if (removedTodos.length) {
this.remove(removedTodos);
}
}
});
\ No newline at end of file
/**
* Handles processing and rendering external XTemplates.
*/
Ext.define('TodoDeftJS.util.TemplateLoader', {
templateRenderer: function (loader, response, active) {
var targetComponent, template, templateConfig;
targetComponent = loader.getTarget();
templateConfig = {};
if ((targetComponent.templateConfig != null)) {
templateConfig = targetComponent.templateConfig;
}
template = new Ext.XTemplate(response.responseText, templateConfig);
targetComponent.tpl = template;
targetComponent.refresh();
}
});
\ No newline at end of file
/**
* To-Do data view.
*/
Ext.define('TodoDeftJS.view.TodoView', {
extend: 'Ext.view.View',
alias: 'widget.todoDeftJS-view-todoView',
controller: 'TodoDeftJS.controller.TodoController',
inject: ['templateLoader', 'todoStore'],
config: {
templateLoader: null,
todoStore: null
},
initComponent: function () {
Ext.apply(this, {
itemSelector: 'li.todo',
store: this.getTodoStore(),
tpl: '',
loader: {
url: 'templates/todolist.tpl',
autoLoad: true,
renderer: this.getTemplateLoader().templateRenderer
},
templateConfig: {
controller: this.getController()
}
});
return this.callParent(arguments);
}
});
\ No newline at end of file
/**
* Viewport shell for the TodoDeftJS application.
*/
Ext.define('TodoDeftJS.view.Viewport', {
extend: 'Ext.container.Viewport',
requires: ['TodoDeftJS.view.TodoView'],
initComponent: function () {
Ext.applyIf(this, {
items: [
{
id: 'todoView'
}, {
xtype: 'todoDeftJS-view-todoView'
}
]
});
return this.callParent(arguments);
}
});
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" class="newtodo" placeholder="What needs to be done?" autofocus>
</header>
<tpl if="values.length">
<section id="main">
<input id="toggle-all" class="toggleall" type="checkbox" {[ this.controller.areAllComplete() ? "checked" : ""]}>
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<tpl for=".">
<li class="todo {[ values.completed ? "completed" : "" ]} {[ values.editing ? "editing" : "" ]}">
<div class="view">
<input class="toggle" type="checkbox" {[ values.completed ? "checked" : ""]}>
<label>{ title }</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{ title }">
</li>
</tpl>
</ul>
</section>
</tpl>
<tpl if="values.length">
<footer id="footer">
<span id="todo-count"><strong>{[ this.controller.incompleteCount() ]}</strong> {[ ( this.controller.incompleteCount() == 1 ) ? "item" : "items" ]} left</span>
<tpl if="this.controller.completedCount()">
<button id="clear-completed" class="clearcompleted">Clear completed ({[ this.controller.completedCount() ]})</button>
</tpl>
</footer>
</tpl>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p><a href="http://www.sencha.com" muse_scanned="true">ExtJS</a> with <a href="http://www.deftjs.com" muse_scanned="true">DeftJS</a> version created by <a href="http://www.briankotek.com" muse_scanned="true">Brian Kotek</a></p>
<p>Part of <a href="http://todomvc.com" muse_scanned="true">TodoMVC</a></p>
</footer>
\ No newline at end of file
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