7
\$\begingroup\$

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 + ">&#10003;</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>
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Overall the design of the code looks fine. Bearing in mind that this code was posted ~3 years ago and you likely have learned quite a bit since then, I have some comments about both the UI and the code.

UI

After adding an item, the text input value persists and the user must manually clear it. Many users would likely welcome having the value cleared after an item is added to the list.

The buttons labeled all, active and complete could be grouped with a label for Show: (and thus it would make sense to move the reset button to a different line) or else have Show added to the beginning of each label's text.

Code

Miscellaneous feedback

The code has a fair amount of comments. The indentation is somewhat inconsistent, as some lines are indented with one tab whereas others have an additional tab (or sometimes two - e.g. Events notify function). The ES-6 Classes could be used to simplify the objects.

Unused variable listContainer

The <ul> element is selected by id in the View function:

var listContainer = document.getElementById("mainList");

but it is never used in that function. The render method selects the elements with that tagName:

var ul = (document.getElementsByTagName("ul"))[0];

Perhaps it would be better to store listContainer on this in the View function and utilize it in the render method.

list items with same id attribute

The code that generates the list items creates buttons foreach item:

li.innerHTML = value + "<button name='remove' id=" + i + ">x</button>" + "<button name='complete' id=" + i + ">&#10003;</button>";

Both buttons will thus have the same id attribute, but those should be unique1. Additionally, for backwards-compatibility (with HTML 4) an id value should start with a letter. Perhaps using data attributes would be a better solution.

Function scope

Variable that in the View function could be eliminated if the event handler function was bound to this via Function.bind(), or converted to an arrow function so it wouldn't get a separate scope.

1https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id

\$\endgroup\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.