An implementation of TodoMVC using ToopJS
This project serves two purposes:
- Provide the community a well known demo application that features the style and features of TroopJS.
- Provide a step-by-step tutorial on how to write a simple TroopJS application.
For one reason or another there are parts of the application that deviates from the original specifications. We've tried to stay as true as possible, but hey - nobody's perfect. The known deviations are:
-
... There should be a
css
folder for styles,js
folder for JavaScript,index.html
for the markup, aimg
folder for images, and third-party JavaScript libraries should be kept injs/libs/
.Ours is located in
js/lib
. As none of the other folders (css
,js
andimg
) were pluralized, we thought that it was silly to do it here. -
When a user enters task editing mode the task in the task list should be changed from a checkbox with a label to a textbox taking up the same area that is filled with the value of the task. The user can enter a new value for the task, and upon hitting enter the task list is returned to its normal display with the new value for the given task.
We do this, but we felt it was natural to do the same when the user removes focus from the input box.
-
Above the task list there should be a "Mark all as complete" checkbox. When checked this checkbox should toggle the state of all the other tasks to match the state of the mark all checkbox. This means that if the mark all checkbox was checked and is unchecked after the user clicks it, all other tasks should be unchecked (marked as incomplete). When there are no tasks present, this checkbox should be completely hidden.
Since the specification does not define what this checkbox should do when only some of the tasks are marked as completed, we've added an indeterminate state that covers this usecase.
This part of the document gives you a step-by-step tutorial on how the todo application was written.
Before we look at any code we'll take you through the (recommended) directory structure for a TroopJS application.
.
├── build
│ └── lib
├── src
│ ├── css
│ └── js
│ └── lib
└── test
As you and see all application sources are contained in a top src
folder. The reason for this is that we want to keep application resources separated from test and build resources. So to that effect, the test
folder contains test related resources and the build
folder contains build related resources.
Inside the js
and build
folder there's a folder called lib
. This is where external libraries should be stored. External libraries should be AMD compliant.
TroopJS makes use of git submodules to manage external libraries. Many of these libraries are not AMD compliant and some of them have platform or tool dependent build systems that would make the build of a TroopJS application prohibitively difficult. To solve this we've created clones of these libraries and committed AMD patches and build output to our clones. This way we can submodule our clones while still tracking upstream changes.
As previously noted the application resources are all contained in the src
folder. In this folder there are a couple of standard folders that most applications would need
src
├── js
│ ├── lib
│ └── widget
├── css
└── img
It's also recommended that there's a src/index.html
(the application landing-page) and src/js/app.js
(the application entry point).
So before we start we'll create a skeleton structure and add the external libraries needed for TroopJS to function.
As previously mentioned submodules should be added using git. For instructions on how to do this you can take a look at the documentation.
After this is done the directory structure will look something like this
src
├── css
└── js
├── lib
│ ├── jquery
│ ├── requirejs
│ └── troopjs-bundle
└── widget
Note that we've omitted the
img
folder as we'll embed all the images in our CSS
So now we can start with our todo application. The first thing we should do is to copy the template resources to the correct locations. Once we're done with this we'll take a look at index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Template - TodoMVC</title>
<link rel="stylesheet" href="../assets/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="todoapp">
<header>
<h1>Todos</h1>
<input id="new-todo" type="text" placeholder="What needs to be done?">
</header>
<!-- this section is hidden by default and you be shown when there are todos and hidden when not -->
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li class="done">
<div class="view">
<input class="toggle" type="checkbox" checked>
<label>Create a TodoMVC template</label>
<a class="destroy"></a>
</div>
<input class="edit" type="text" value="Create a TodoMVC template">
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>Rule the web</label>
<a class="destroy"></a>
</div>
<input class="edit" type="text" value="Rule the web">
</li>
</ul>
</section>
<!-- this footer needs to be shown with JS when there are todos and hidden when not -->
<footer>
<a id="clear-completed">Clear completed</a>
<div id="todo-count"></div>
</footer>
</div>
<div id="instructions">
Double-click to edit a todo.
</div>
<div id="credits">
Created by <a href="http://addyosmani.github.com/todomvc/">you</a>.
</div>
<!-- scripts here -->
<script src="js/app.js"></script>
</body>
</html>
First we'll have to adjust the head
section to run our application in "stand-alone" mode.
<link rel="stylesheet" href="css/app.css">
Since the template did not include the
base.css
we'll copy it the from the original into ourcss
folder. At the same time we should add acss/app.css
with a@import url("base.css")
. The reason for using@import
instead of having twolink
elements is that eventually we'll want to run an optimizer on this project, and the optimizer understands@import
, but not individuallink
.
And after that we should set up our application entry point
<script type="text/javascript" data-main="js/app.js" src="js/lib/requirejs/require.js"></script>
TroopJS uses RequireJS for its dependency management. The recommended way to bootstrap a RequireJS application is described here
Let's add a src/app.js
require({
"baseUrl" : "js",
"paths" : {
"jquery" : "lib/jquery/dist/jquery",
"troopjs-bundle" : "lib/troopjs-bundle/dist/troopjs-bundle-mini.min"
},
"deps": [ "troopjs-bundle" ]
}, [ "jquery" ], function App(jQuery) {
jQuery(document).ready(function ready($) {
$(this.body).find("[data-weave]").weave();
});
});
Lets review
-
require({
Start configuring RequireJS
RequireJS supports a configuration object as the first argument to the
require
function. -
"baseUrl" : "js",
Set the
baseUrl
tojs
.baseUrl: the root path to use for all module lookups. So in the above example, "my/module"'s script tag will have a src="/another/path/my/module.js". baseUrl is not used when loading plain .js files, those strings are used as-is, so a.js and b.js will be loaded from the same directory as the HTML page that contains the above snippet.
If no baseUrl is explicitly set in the configuration, the default value will be the location of the HTML page that loads require.js. If a data-main attribute is used, that path will become the baseUrl.
The baseUrl can be a URL on a different domain as the page that will load require.js. RequireJS script loading works across domains. The only restriction is on text content loaded by text! plugins: those paths should be on the same domain as the page, at least during development. The optimization tool will inline text! plugin resources so after using the optimization tool, you can use resources that reference text! plugin resources from another domain.
-
"paths" : { "jquery" : "lib/jquery/dist/jquery", "troopjs-bundle" : "lib/troopjs-bundle/dist/troopjs-bundle-mini.min" },
Configure application path 'aliases'.
paths: path mappings for module names not found directly under baseUrl. The path settings are assumed to be relative to baseUrl, unless the paths setting starts with a "/" or has a URL protocol in it ("like http:"). In those cases, the path is determined relative to baseUrl. Using the above sample config, "some/module"'s script tag will be src="/another/path/some/v1.0/module.js". The path that is used for a module name should not include the .js extension, since the path mapping could be for a directory. The path mapping code will automatically add the .js extension when mapping the module name to a path.
-
"deps": [ "troopjs-bundle" ]
Depend on
troopjs-bundle
deps: An array of dependencies to load. This is useful when require is defined as a config object before require.js is loaded, and you want to specify dependencies to load as soon as require() is defined.
-
}, [ "jquery" ], function App(jQuery) {
The second argument to
require
is an array of dependencies.Just like
define
the array of dependencies is passed to the module entry point as arguments -
jQuery(document).ready(function ready($) {
Add a standard ready handler to the document
-
$(body).find("[data-weave]").weave(dfdStart);
Find all children of the
body
element that havedata-weave
attributes and weave them. Wrap all of this in adeferred
so we can get a callback when everything is done.
Now we've configure our application to use RequireJS and set up the application entry point.
Lets go back and look at index.html
. We want to try to break out functionality into small (somewhat self-contained) widgets, and the natural place to start is adding and displaying todo items.
There are three main classes of modules in TroopJS
component
s are the base building block of anything TroopJSgadget
s extendcomponent
s with methods likepublish
andajax
widget
s extendgadget
s with UI related methods likehtml
andtrigger
Let's do this by adding weave instructions in the HTML using data-weave
attributes.
-
<input id="new-todo" type="text" placeholder="What needs to be done?" data-weave="widget/create">
-
<ul id="todo-list" data-weave="widget/list">
TroopJS weaves widgets to the DOM by traversing it and finding elements that have a
data-weave
attribute. When weaving an element TroopJS will:
- Locate (and if needed async load) the module containing the widget
- Instantiate the widget (if needed, we do support singleton widgets)
- Wire the instance (basically reflect on the instance and scan for well-known method signatures), more on this later
The first widget to deal with is `widget/create.js'
Widgets are named after where they are located (relative to
baseUrl
) in the source tree. A general rule is to simply add.js
to the widget name to locate the file, sowidget/create
can be found insrc/js/widget/create.js
define( [ "troopjs/component/widget" ], function CreateModule(Widget) {
return Widget.extend({
"dom/keyup" : function onKeyUp(topic, $event) {
var self = this;
var $element = self.$element;
switch($event.keyCode) {
case 13:
self.publish("todos/add", $element.val());
$element.val("");
}
}
});
});
Let's go through this widget
-
define( [ "troopjs/component/widget" ], function CreateModule(Widget) {
Start the definition of this module and declare its dependencies. The module is (internally) named
CreateModule
and it depends ontroopjs/component/widget
which will be available inside the module asWidget
If you look above in
src/js/app.js
you'll find a path definition fortroopjs
that points tolib/troopjs/src
. This means thattroopjs/...
actually resolves tolib/troopjs/src/...
-
return Widget.extend({
The result of this module is extending
Widget
Support for
.extend
is provided by ComposeJS. TroopJS uses ComposeJS for all its object composition -
"dom/keyup" : function onKeyUp(topic, $event) {
This is where wiring becomes important. As mentioned above, wiring scans for well-known method signatures, and
dom/*
is one of these. In this instance, we're indicating that we want to add a handler for the DOMkeyup
event.All wired handlers always get a
topic
as the first argument. The topic contains information on what the trigger to this handler was. The rest of the arguments vary depending on the type of trigger. For DOM triggers, the second argument is the original jQuery event object. -
var self = this; var $element = self.$element; switch($event.keyCode) { case 13: self.publish("todos/add", $element.val()); $element.val(""); }
- Save
this
asself
so we can use it inside of closures - Save
self.$element
(woven element) as$element
- Check if the
keyCode
of the event was enter - if sopublish
the value ontodos/add
and then reset.
- Save