In this section we build our first app, and discuss the various steps in creating an app like that.
We start with a high-level description of the app we have in mind. It will be an application that helps us manage a list of “tasks”. Here is some desired functionality:
We will look at optional behavior later.
Before we describe one approach, you should work on it first. You should answer the following questions:
Here are some general principles to adhere to:
Based on the above description, let us consider some key components of such a project:
We will need to have some “Task” objects. They should contain a title, a completion status, and a list of labels. Tasks objects have methods for adding/removing a label, setting the title and so on.
In an extended version of the app, tasks might be stored in some way, for instance in a database.
One important consideration is where the information about whether a task is visible is stored. Should it be part of the task objects, or does it belong in a separate place, that is responsible for the UI?
We will need to have a way to manage a list of objects. This could be as simple as an array underneath, but we will provide a specific interface.
A taskList should have ways to add/remove tasks, and a way iterate over the tasks.
Deciding what to do with labels is a hard decision. Here are some desirable features:
We have for the most part two design options:
The constructor for label objects will be unusual in the sense that for any given string there can be only one label for that string. Our constructor will therefore be enforcing that, and possibly return an existing object if it is asked to create an existing label.
Because of this unusual behavior of labels, we will not use a separate object to manage the list of labels. Instead, the Label
class will be providing us with a “list” view of the labels when asked.
We’ll need some structure to keep track of the filters that are in place. These would include the list of the labels, and whether we should show only un-completed or only completed tasks.
A key question is who is responsible for deciding if a task should be shown or not. Is this a question that the tasks should be able to answer for themselves, given a Filters structure, or is this a question that the Filters structure should be able to answer, given a task? The latter makes more sense.
A controller is the component responsible for responding to user input, adjusting the backend structures as needed, as well as updating the interface.
In more complex applications we may have multiple controllers. In this instance we will use a single controller, that manages all the different page parts.
Here’s a basic graph for an initial setup for the different modules:
We need to decide how to organize our code. Here are some key considerations:
We will need to decide on a file structure. We will use the following:
- projectRoot
- css
cssfiles in here
- js
javascript files in here
- lib
folder for 3rd-party libraries like jQuery
- test
test files go here
index.html
.eslintrc.json
other key root level files
This is by no means the only way to do it, but it is one way.The functionality that actually requires a web browser should be restricted to the Controller. The other classes (models), should be self-sufficient, and we should be able to write tests for them directly.
We now take a look at the starting layout code for our app.
We start with index.html
:
<!DOCTYPE html>
<html>
<head>
<title>TaskApp: The Task App of the future</title>
<link rel="stylesheet" type="text/css" href="css/reset.css">
<link rel="stylesheet" type="text/css" href="css/taskapp.css">
</head>
<body>
<header>
</header>
<main>
<section id="filters">
<div class="filter" id="postsChoice">
<h3>Posts to show</h3>
<!-- TODO: Add radio buttons -->
</div>
<div class="filter" id="labelsList">
<h3>Labels</h3>
<ul>
<!-- Labels will be added dynamically using the template -->
</ul>
</div>
</section>
<section id="tasks">
</section>
</main>
<footer>
</footer>
<!-- Normal page info ends here. Some scripts load further down. -->
<!-- This is the template for a task. It is not actually treated as Javascript code
the curly braces are placeholders for dynamically generated values -->
<script id="taskTemplate" type="text/template">
<div class="task">
<p>{{title}}</p><input class="completed" type="checkbox" value="{{completed}}"></input>
</div>
</script>
<script id="labelTemplate" type="text/template">
<div class="label">
<p>{{tag}}<span><!-- count goes here --></span></p><input class="completed" type="checkbox" value="{{selected}}"></input>
</div>
</script>
<!-- SCRIPT LOADING HERE -->
<script type="text/javascript" src="js/templates.js"></script>
<script type="text/javascript" src="js/tasklist.js"></script>
<script type="text/javascript" src="js/label.js"></script>
<script type="text/javascript" src="js/task.js"></script>
<script type="text/javascript" src="js/filters.js"></script>
<script type="text/javascript" src="js/controller.js"></script>
</body>
</html>
Our goal for now is to:
Next we look at the template file. It provides us with methods for handling our primitive version of templates. Later on we’ll make it use a template library.
// templates.js
//
// This file handles the various templates for us.
//
(function(global) {
var Template, templateStorage, proto;
if (!global.hasOwnProperty('TaskApp')) {
global.TaskApp = {};
}
// Object keeping the stored templates
templateStorage = {};
/*
* Exported object. `load` is used to store a template, `parse` is used to
* parse a stored template.
*/
Template = {
/*
* Returns a new template object based on the text in `html`.
* The `name` can be used to access the template in the future
* If a template with the same name exists, it prints a warning message
* and replaces it.
*/
new: function newTemplate(name, html) {
},
/*
* Returns the template with a given name, or `null` if it does not exist.
*/
get: function getTemplate(name) {
}
};
/*
* Prototype object for created templates
*/
proto = {
/*
* Parses the template named `name`, using the `values` object
* to resolve parameter entries. For instance {{foo}} will be
* replaced by values.foo
*/
parse: function(values) {
}
};
global.TaskApp.Template = Template;
}(typeof window === 'undefined' ? root : window));