Skip to content

Tutorials Build A Task List Page 2

gburghardt edited this page Oct 2, 2013 · 3 revisions

Note: This is a continuation of Introduction to Modules: Build a Task List

On the previous page we created a task list module that mapped DOM events to methods on TaskListModule, defined some DOM nodes inside our module that we need frequent access to using the ElementStore, and we finished the "add" method so the user can actually add a task to the list.

Now let's allow the user to remove a task they have already added.

Removing a Task List Item

Previously, we added the "actions" property to TaskListModule. We have already mapped "click" events to the "remove" method:

var TaskListModule = Module.extend({

    prototype: {

        actions: {
            click: [
                "remove",
                "toggleComplete"
            ],
            submit: [
                "add"
            ]
        },

        ...
    }

});

Let's finish defining the "remove" method.

var TaskListModule = Module.extend({

    prototype: {

        actions: {
            ...
        },

        elementStore: {
            ...
        },

        add: function add(event, element, params) {
            ...
        },

        remove: function remove(event, element, params) {
            event.stop();
            var item;

            if (confirm("Are you sure you want to remove this task?")) {
                item = element.parentNode;
                item.parentNode.removeChild(item);
            }

            event = element = params = item = null;
        },

        ...
    }
});

The notable difference here is how we get a reference to the <li> tag that represents a task list item. First, let's recall what the HTML structure of this module is like:

<div data-modules="TaskListModule">
    <form>...</form>

    <ol>
        <li>
            <input type="checkbox" data-action="toggleComplete">
            <span>Task text</span>
            <button type="button" data-action="remove">X</button>
        </li>
    </ol>

    <script>...</script>
</div>

The <ol> tag holds all the tasks added by the user. Each task item is represented as an <li> tag. When the user clicks on the <button> tag, Foundry takes the value of the data-action attribute and calls the "remove" method on TaskListModule. Previously we found out that event handlers in Foundry get passed three parameters:

  1. event — The browser event object
  2. element — The element with the data-action attribute, which in this case is the <button>
  3. params — Currently an empty object

We know the <button> is contained directly inside the <li>. We want to remove the <li> from the document. We only need the standard Document Object Model API to remove this item from the list.

remove: function(event, element, params) {
    ...

    if (confirm("Are you sure you want to remove this task?")) {
        item = element.parentNode;
        item.parentNode.removeChild(item);
    }

    ...
}

Done!

Now we can add and remove tasks, but it's kind of nice to see which tasks were completed.

Completing a Task

Since we have already mapped the "click" event to the "toggleComplete" method in the first page, we only need to finish defining the "toggleComplete" method.

We don't want to remove a task once it is completed. We just need to visually show the user that it has been completed. A simple strike-through is sufficient. With a little splash of CSS and an HTML tag class name, we can mark items as "complete".

If you look at the top of your demo page in the <head> you'll notice a <style> tag. It declares a CSS class called .done. All we need to do is add the "done" class to the <li> tag.

Being the smart little programmers we are, we don't want to hard code the "done" class in our JavaScript. Since this is merely style, we can add this as an "option" in TaskListModule. This CSS class name can be overridden at run time if need be.

var TaskListModule = Module.extend({

    prototype: {

        actions: {
            ...
        },

        elementStore: {
            ...
        },

        options: {
            doneClass: "done"
        },

        ...

        toggleComplete: function toggleComplete(event, element, params) {
            event.stopPropagation();

            var regex = new RegExp("(^|\\s+)(" + this.options.doneClass + ")(\\s+|$)");
            var item = element.parentNode;

            if (!element.checked) {
                item.className = item.className.replace(regex, "$1$3")
                                               .replace(/[\s]{2,}/g, " ");
            }
            else {
                item.className = item.className + " " + this.options.doneClass;
            }

            event = element = item = params = null;
        }

    }

});

The "options" property in the class definition provides some default values for this.options at runtime.

Let's take a look at the HTML structure we are working with:

<div data-modules="TaskListModule">
    <form>...</form>

    <ol>
        <li>
            <input type="checkbox" data-action="toggleComplete">
            ...
        </li>
    </ol>

    <script>...</script>
</div>

The checkbox is a direct child of the <li>. Since the checkbox has the data-action attribute on it, the "element" variable in the toggleComplete method references the checkbox. The <li> tag is accessible by using element.parentNode. Then it's just a matter of adding or removing this.options.doneClass from the <li> tag to mark a task as done, or unmark it.

Our Progress So Far

We've added two methods to remove a task and mark it as complete, or unmark it as complete. Additionally, we added a runtime configurable option called "doneClass" available through this.options.doneClass so we can override this CSS class name if need be.

There have been no changes to the underlying HTML, so let's just review what the TaskListModule class should look like:

var TaskListModule = Module.extend({

    prototype: {

        actions: {
            click: [
                "remove",
                "toggleComplete"
            ],
            submit: [
                "add"
            ]
        },

        elementStore: {
            elements: {
                form: { selector: "form" },
                itemRoot: { selector: "ol" },
                template: { selector: "script.template" }
            }
        },

        options: {
            doneClass: "done"
        },

        add: function add(event, element, params) {
            event.stop();

            var form = this.form();
            var taskTextField = form.elements.task;
            var taskText = taskTextField.value;
            var item;
            var markup;

            if (/^\s*$/.test(taskText)) {
                alert("Please enter the task to add.");
            }
            else {
                markup = this.template().innerHTML;
                item = this.elementStore.parseHTML(markup)[0];
                item.getElementsByTagName("span")[0].innerHTML = taskText;
                this.itemRoot().appendChild(item);
            }

            taskTextField.value = "";
            taskTextField.focus();
            taskTextField.select();

            event = element = params = item = taskTextField = null;
        },

        remove: function remove(event, element, params) {
            event.stop();
            var item;

            if (confirm("Are you sure you want to remove this task?")) {
                item = element.parentNode;
                item.parentNode.removeChild(item);
            }

            event = element = params = item = null;
        },

        toggleComplete: function toggleComplete(event, element, params) {
            event.stopPropagation();

            var regex = new RegExp("(^|\\s+)(" + this.options.doneClass + ")(\\s+|$)");
            var item = element.parentNode;

            if (!element.checked) {
                item.className = item.className.replace(regex, "$1$3")
                                               .replace(/[\s]{2,}/g, " ");
            }
            else {
                item.className = item.className + " " + this.options.doneClass;
            }

            event = element = item = params = null;
        }

    }

});

Now we have a module that does one thing, and does it well. Let's explore how modules in different root elements can interact with one another through Application Events.

< Back | Wiki Home | Next >