I just started learning JavaScript and I have created a basic TODO list following MVC concepts to apply what I have learnt so far. This is probably overkill for a simple todo app, but the objective is to improve my JavaScript skills.
I'd highly appreciate it if you can spot flaws, especially bad practices and suggest improvements/alternative approaches and basically anything that can help me improve myself further.
Overview of the design
The view
communicates user interactions to the controller
via the observer pattern
. The controller
notifies the model
. The model
updates the data representation and notifies the controller
of the change via the observer pattern
. The controller
then asks the view
to re-render itself
event.js
Implements the observer pattern for a specific event. Allows listeners to be attached and notified via prototype functions
//Event Dispatcher. maintains a list of observers for a particular event.
//Used for communication between view-controller and model-controller
function Event(notifier){
this.notifier = notifier;
this.listeners = [];
}
//methods to attach and notify listeners
Event.prototype = {
attach: function(listener){this.listeners.push(listener);
},
notify: function(val1,val2){
for (var i=0; i<this.listeners.length; i++){
(this.listeners[i])(val1,val2);}
}
}
item.js
Object representation of an item in a list. Contains additional information for functionality implementation
//object representation of an item
function TodoItem(item){
this.item = item; //item name
this.isActive = true; //state maintains active status of item
this.visible = true; //controls visibility of item in the view
}
model.js
Maintains an array of items. Handles the data and the business logic. Notifies the controller of model modification through the observer pattern
//MODEL maintains data and handles business logic
function Model(){
//maintains items added to the list
this.list = [];
//Item addition event. Contains observers (controller)
this.listModifiedEvent = new Event(this);
}
Model.prototype = {
//add item to list and notify observers
addItem: function (item){
this.list.push(item);
this.listModifiedEvent.notify(this.list);
},
//remove item from list and notify observers
removeItem: function (id){
this.list.splice(id,1);
this.listModifiedEvent.notify(this.list);
},
//mark item specified by id as complete and notify observers
completeItem: function (id){
var item = this.list[id];
item.isActive= !item.isActive;
this.listModifiedEvent.notify(this.list);
},
//show list with all items, complete and active
showAll: function(){
for (var i=0; i<this.list.length; i++){
this.list[i].visible=true;
}
this.listModifiedEvent.notify(this.list);
},
//sets the visible property of items to true if active, false otherwise. notifies observers
showActive: function(){
for (var i=0; i<this.list.length; i++){
var item = this.list[i];
if (item.isActive)item.visible=true;
else item.visible=false;
}
this.listModifiedEvent.notify(this.list);
},
//sets the visiblity property of items to true if complete, false otherwise. notifies observers
showComplete: function(){
for (var i=0; i<this.list.length; i++){
var item = this.list[i];
if (!item.isActive)item.visible=true;
else item.visible=false;
}
this.listModifiedEvent.notify(this.list);
},
clear: function(){
this.list.length = 0;
this.listModifiedEvent.notify(this.list);
},
saveToLocal: function(){
console.log("save: " + JSON.stringify(this.list));
localStorage.setItem("todoItemsList", JSON.stringify(this.list));
},
remember: function(){
var localStore = localStorage.getItem("todoItemsList");
if (localStore){
this.list = JSON.parse(localStore);
}
}
}
view.js
Listeners attached to handle user interactions. Communicates with the controller via the observer pattern. Renders the view once notified by the controller.
//maintains the view. handles rendering
function View(){
var that = this;//save this context for callbacks
//user interaction events. Event object used to implement observer pattern
this.listButtonEvent = new Event(this);
//retrieve dom elements. private
var addButton = document.getElementById("addButton");
var listContainer = document.getElementById("mainList");
var bodyElement = document.body;
//list modification events. add/delete/complete/show lists
//concept of event propogation used to catch button events at the body element
bodyElement.addEventListener('click',function(event){
var el = event.target; //element that caused event propogation
var input = null;
if (el.nodeName=="BUTTON"){
//if the event is an add operation, pass user input as second arg
if (el.name=="add")input=that.userInput();
that.listButtonEvent.notify(el,input);
}
});
//function to extract text input
this.userInput = function(){
return document.getElementById("input").value;
}
}
//method to render the view. whole list is re-rendered everytime this method is called
View.prototype.render = function(list){
var ul = (document.getElementsByTagName("ul"))[0];
ul.innerHTML = "";
for (var i=0; i<list.length; i++){
//build li element and append to ul
var listItem = list[i];
//only build elements that are visible. controls active and complete task visibility
if (listItem.visible){
var value = listItem.item;
var li = document.createElement("li");
li.innerHTML = value + "<button name='remove' id=" + i + ">x</button>" + "<button name='complete' id=" + i + ">✓</button>";
//cross out completed items
if (!listItem.isActive)
li.style.setProperty("text-decoration", "line-through");
ul.appendChild(li);
}
}
}
controller.js
Fascilitates communication between the view and the model. Contains references to both the view and the model. Subscribed as an observer to both. (Retrieves user interactions from the view and communicates them to the model. Notifies the view to render itself when the model changes)
//fascilitates communication between the view and the model, data processing
function Controller(model,view){
//process list button events. delete/complete/show lists
this.processListButtonEvent =
function(el,input){
var buttonName = el.name;
//if input is valid, create an item object and add it to the model
if (buttonName=="add"){
if(input){
model.addItem(new TodoItem(input));
}
}
//remove item from model
else if (buttonName=="remove")model.removeItem(el.id);
//mark items as complete
else if (buttonName=="complete"){
model.completeItem(el.id);}
//generate all tasks list
else if (buttonName=="list"){
model.showAll();
}
//generate active tasks list
else if (buttonName=="activeList"){
model.showActive();
}
//generate complete tasks list
else if (buttonName=="completeList"){
model.showComplete();
}
//clear list
else if (buttonName=="reset"){
model.clear();
}
}
//model modified. update the view
this.listModified =
function(list){
view.render(list);
}
//Controller added as observer to events in the view and the model
//controller notified via observer pattern on view events and model modification
view.listButtonEvent.attach(this.processListButtonEvent);
model.listModifiedEvent.attach(this.listModified);
}
todomvc.js
Initializes the app. Local storage used to remember state
//initialize app
var model = new Model();
var view = new View();
var controller = new Controller(model,view);
//save/load item list from local storage
window.onload = loadOnStart;
window.onbeforeunload = function(){
saveOnExit();
return null;
}
//load item list from local storage and render page
function loadOnStart(){
model.remember();
model.showAll();
}
//save item list to local storage on exit. remember state
function saveOnExit(){
model.saveToLocal();
}
index.html
<!DOCTYPE html>
<html>
<head>
<style></style>
</head>
<body>
<input id="input" type="text">
<button id="addButton" name="add">add</button>
<ul id="mainList">
</ul>
<button id="all" name="list" >all</button>
<button id="activeButton" name="activeList" >active</button>
<button id="completeButton" name="completeList" >complete</button>
<button id="resetButton" name="reset" >reset</button>
</body>
<script src="event.js"></script>
<script src="item.js"></script>
<script src="model.js"></script>
<script src="view.js"></script>
<script src="controller.js"></script>
<script src="todomvc.js"></script>
</html>