{{announcement.body}}
{{announcement.title}}

The TodoMVC Revisited

DZone 's Guide to

The TodoMVC Revisited

In this article, we explore the use of a state machine for the development of the TodoMVC UI application

· Web Dev Zone ·
Free Resource

The TodoMVC UI (todomvc.com) application is widely considered to be the "Hello World" for UI development. The site has several demos using various UI frameworks. In this article, I explore the use of a state machine for the development of the TodoMVC UI application. I'll use the vanilla JavaScript to illustrate these concepts, and I'll use the state machine framework that I presented in a previous article.

State Transitions

The first step in using the state machine is to write the state transitions for the UI of our application. I'll assume the following state transitions for the TodoMVC app (screen capture is at the bottom of the page) that I am considering here:

Initials State Pre-Event Processor Post-Event Final State
unknownState onload processOnload() onloadSuccess readyForAdd
readyForAdd
addTodo
processAddTodo()
addTodoSuccessNoneSelected
readyForAddSelect
readyForAddSelect addTodo
processAddTodo()
addTodoSuccessNoneSelected
readyForAddSelect
readyForAddSelect
changeTodo
processChangeTodo()
changeTodoSuccessSomeSelected
readyForAddSelectUnselectDelete
readyForAddSelect
changeTodo
processChangeTodo()
changeTodoSuccessAllSelected
readyForAddUnselectDelete
readyForAddUnselectDelete
addTodo
processAddTodo()
addTodoSuccessSomeSelected
readyForAddSelectUnselectDelete
readyForAddUnselectDelete
changeTodo
processchangeTodo()
changeTodoSuccessNoneSelected
readyForAddSelect
readyForAddUnselectDelete
changeTodo
processchangeTodo()
changeTodoSuccessSomeSelected
readyForAddSelectUnselectDelete
readyForAddUnselectDelete
deleteTodo
processDeleteTodo()
deleteTodoSuccessAllDeleted
readyForAdd
readyForAddSelectUnselectDelete
addTodo
processAddTodo()
addTodoSuccessSomeSelected
readyForAddUnselectDelete
readyForAddSelectUnselectDelete
changeTodo
processChangeTodo()
changeTodoSuccessAllSelected
readyForAddUnselectDelete
readyForAddSelectUnselectDelete
changeTodo
processChangeTodo()
changeTodoSuccessSomeSelected
readyForAddSelectUnselectDelete
readyForAddSelectUnselectDelete
changeTodo
processChangeTodo()
changeTodoSuccessNoneSelected
readyForAddSelect
readyForAddSelectUnselectDelete
changeTodo
processChangeTodo()
changeTodoSuccessSomeSelected
readyForAddSelectUnselectDelete
readyForAddSelectUnselectDelete
deleteTodo
processDeleteTodo()
deleteTodoSuccessNoneSelected
readyForAddSelect


I have identified four application states: readyForAddreadyForAddSelect, readyForAddUnselectDelete, and readyForAddSelectUnselectDelete. The state readyForAdd, for instance, implies only add events can be emitted from this state, while the readyForAddSelect state can only emit add and select events.

You may also like: Model-View-Controller (MVC) Deep Dive.

Events and State Configuration

The next step is to configure the events and states captured in the above table as JavaScript objects. The events are captured like:

JavaScript




xxxxxxxxxx
1
110


 
1
const appEvents = {
2
    onload: {
3
        process: function(e, handlePostEvent) {
4
            var props = {
5
                id: "addTodo",
6
                todoText: "",
7
                label: "Add a to-do item:"
8
            };
9
            createInputTextElement(document.getElementById("addTodo"), props);
10
            handlePostEvent(new CustomEvent('onloadSuccess'));
11
        }
12
    },
13
    onloadSuccess: {
14
        nextState: function(e) {
15
            return appStates.readyForAdd(e);
16
        }
17
    },
18
    addTodo: {
19
        process: function(e, handlePostEvent) {
20
 
          
21
            e.detail.id = "changeTodo";
22
            e.detail.todoText = appData.todoText();
23
            e.detail.itemsCount = appData.itemsCount();
24
            createInputCheckboxElement(document.getElementById("changeTodo"), e.detail);
25
 
          
26
            var evttype = 'addTodoSuccessNoneSelected';
27
            if (appData.selectedCount() > 0 &&
28
                appData.itemsCount() - appData.selectedCount() > 0) {
29
                evttype = 'addTodoSuccessSomeSelected';
30
            }
31
            handlePostEvent(new CustomEvent(evttype));
32
        }
33
    },
34
    addTodoSuccessNoneSelected: {
35
 
          
36
        nextState: function(e) {
37
 
          
38
            return appStates.readyForAddSelect(e);
39
        }
40
    },
41
    addTodoSuccessSomeSelected: {
42
 
          
43
        nextState: function(e) {
44
            return appStates.readyForAddSelectUnselectDelete(e);
45
        }
46
    },
47
    changeTodo: {
48
        process: function(e, handlePostEvent) {
49
            if (document.getElementById("deleteTodo").getElementsByTagName("input").length == 0) {
50
                e.detail.id = "deleteTodo";
51
                createInputButtonElement(document.getElementById("deleteTodo"), e.detail);
52
            }
53
            var evttype = 'changeTodoSuccessNoneSelected';
54
            if (appData.selectedCount() > 0) {
55
                if (appData.selectedCount() == appData.itemsCount()) {
56
                    evttype = 'changeTodoSuccessAllSelected';
57
                } else if (appData.itemsCount() - appData.selectedCount() > 0) {
58
                    evttype = 'changeTodoSuccessSomeSelected';
59
                }
60
            }
61
            handlePostEvent(new CustomEvent(evttype));
62
        }
63
    },
64
    changeTodoSuccessSomeSelected: {
65
 
          
66
        nextState: function(e) {
67
            return appStates.readyForAddSelectUnselectDelete(e);
68
        }
69
    },
70
    changeTodoSuccessAllSelected: {
71
 
          
72
        nextState: function(e) {
73
            return appStates.readyForAddUnselectDelete(e);
74
        }
75
    },
76
    changeTodoSuccessNoneSelected: {
77
 
          
78
        nextState: function(e) {
79
            return appStates.readyForAddSelect(e);
80
        }
81
    },
82
    deleteTodo: {
83
        process: function(e, handlePostEvent) {
84
            e.detail.id = 'changeTodo';
85
            e.detail.itemsCount = appData.itemsCount();
86
            e.detail.selTodos = appData.selectedItems();
87
            deleteInputCheckboxElements(document.getElementById("changeTodo"), e.detail);
88
            let evttype = '';
89
            if (appData.itemsCount() > 0 &&
90
                    appData.selectedCount()==0) {
91
                evttype = 'deleteTodoSuccessNoneSelected';
92
            }
93
            else evttype = 'deleteTodoSuccessAllDeleted';
94
            handlePostEvent(new CustomEvent(evttype));
95
        }
96
 
          
97
    },
98
    deleteTodoSuccessNoneSelected: {
99
        nextState: function(e) {
100
            return appStates.readyForAddSelect(e);
101
        }
102
    },
103
 
          
104
    deleteTodoSuccessAllDeleted: {
105
        nextState: function(e) {
106
            return appStates.readyForAdd(e);
107
        }
108
    }
109
 
          
110
};


 
The application states are captured like:

JavaScript




xxxxxxxxxx
1
34


 
1
const appStates = {
2
    readyForAdd: function(e) {
3
        document.getElementById("addTodoView").style.display = "block";
4
        document.getElementById("changeTodoView").style.display = "none";
5
        document.getElementById("deleteTodoView").style.display = "none";
6
        document.getElementById("currentState").innerHTML = e.type + ' => readyForAdd';
7
    },
8
    readyForAddSelect: function(e) {
9
        document.getElementById("addTodo").getElementsByTagName("input")[0].value = "";
10
        document.getElementById("addTodo").getElementsByTagName("input")[0].focus();
11
        document.getElementById("addTodoView").style.display = "block";
12
        document.getElementById("changeTodoView").style.display = "block";
13
        document.getElementById("deleteTodoView").style.display = "none";
14
        document.getElementById("currentState").innerHTML = e.type + ' => readyForAddSelect';
15
    },
16
    readyForAddUnselectDelete: function(e) {
17
        document.getElementById("addTodo").getElementsByTagName("input")[0].value = "";
18
        document.getElementById("addTodo").getElementsByTagName("input")[0].focus();
19
        document.getElementById("addTodoView").style.display = "block";
20
        document.getElementById("changeTodoView").style.display = "block";
21
        document.getElementById("deleteTodoView").style.display = "block";
22
        document.getElementById("currentState").innerHTML = e.type + ' => readyForAddUnselectDelete';
23
    },
24
    readyForAddSelectUnselectDelete: function(e) {
25
        document.getElementById("addTodo").getElementsByTagName("input")[0].value = "";
26
        document.getElementById("addTodo").getElementsByTagName("input")[0].focus();
27
        document.getElementById("addTodoView").style.display = "block";
28
        if (appData.itemsCount() > 0) document.getElementById("changeTodoView").style.display = "block";
29
        else document.getElementById("changeTodoView").style.display = "none";
30
        document.getElementById("deleteTodoView").style.display = "block";
31
        document.getElementById("currentState").innerHTML = e.type + ' => readyForAddSelectUnselectDelete';
32
    }
33
};
34
 
          



View — HTML Template

The HTML template that I consider for this demo is:

HTML




xxxxxxxxxx
1
19


 
1
    <div class="pure-g">
2
        <div class="pure-u-1-4"> </div>
3
        <section id="todoApp" class="pure-u-1-2">
4
            <div id="currentState" class="red, small"> </div>
5
            <div> </div>
6
            <section id="addTodoView" style="display: none;">
7
                <InputComp id="addTodo"></InputComp>
8
            </section>
9
            <section id="changeTodoView" style="display: none;">
10
                <p></p>
11
                <CheckboxGroupComp id="changeTodo" maxId="0"></CheckboxGroupComp>
12
                <p></p>
13
            </section>
14
            <section id="deleteTodoView" style="display: none;">
15
                <ButtonComp id="deleteTodo"></ButtonComp>
16
            </section>
17
        </section>
18
        <div class="pure-u-1-4"> </div>
19
    </div>



I am using some custom utility tags, like InputCompCheckboxGroupComp, and ButtonComp. The JavaScript code loads the dynamically generated HTML DOM content into these elements.

Model

The model for the application is:

JavaScript




xxxxxxxxxx
1
14


 
1
const appData = {
2
    todoText: function() {
3
        return document.getElementsByName("addTodo").item(0).value;
4
    },
5
    selectedCount: function() {
6
        return appData.selectedItems().length;
7
    },
8
    itemsCount: function() {
9
        return document.getElementsByName("changeTodo").length;
10
    },
11
    selectedItems: function() {
12
        return Array.from(document.getElementsByName("changeTodo")).filter(e => e.checked).map(e => e.value);
13
    }
14
};



Controller

The controller, stateTransitionsManager(), receives all the application emitted events and dispatches them to a processor, along with a callback function (handlePostEvent()). When the processor completes its task (possibly long-running), it invokes the callback function by passing it a new custom event as an argument.

JavaScript




xxxxxxxxxx
1


 
1
function stateTransitionsManager(todoEvent) {
2
    var todoEventAft = appEvents[todoEvent.type].process(todoEvent, handlePostEvent); 
3
}
4
5
function handlePostEvent(e) {
6
    appEvents[e.type].nextState(e);
7
}



The complete source for the demo is on GitHub.

An online demo for this implementation is available at the Todo App.

The demo page also echoes the state transition for each state as info text. The user should be able to walk through the various transitions listed in the table above. Here are the screen captures from the above demo, corresponding to the four states identified in the table:

 readyForAdd 
ready for add event readyForAddSelect 

ready for add select event readyForAddUnselectDelete 

Ready for add unselect delete event

 readyForAddSelectUnselectDelete 

Ready for add select unselect delete

Conclusions

The proposed state machine based UI development pattern is shown to provide a clean and simple framework for the development of robust UI applications. Once the states and events are identified, development is fairly straight-forward. 

Related Works

Interested readers may also checkout davidkpiano, lucaszmakuch,  FrancisStokesimmerjs, and overmindjs. Readers interested in applying similar techniques for backend Java Spring Boot applications can checkout this DZone article.

Topics:
todo app ,mvc ,state machine ,vanilla javascript ,web dev

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}