Task List Project – Sprint 2b

Using Local Storage

Building from the Sprint 1 edition, this Sprint enhances the HTML page through the introduction of Web Storage (specifically Local Storage) to give the impression of data persistence. I same “impression” for the following reasons:

  1. Local Storage is “local” so the data is maintained on the client machine and is not available from anywhere else.
  2. The storage is specific to each browser and is not shared.
  3. Another consideration is that Web Storage is a relatively new addition to the HTML5-family so may not be available on your browser of choice.

Mark 1 – Storage of the HTML

As a mark 1 update, the Local Storage will merely persist the HTML that forms the Task list. When the application load the Task List is populated from Local Storage using the following command, once the objList is located in the application start-up function.

objList_1.innerHTML = 
	localStorage.getItem("ToDoListHTML");

When the list is modified (Task change, added or deleted) and the HTML refreshed, a copy is preserved back in the Local Storage.

localStorage.setItem("ToDoListHTML", 
	document.getElementById("list_1").innerHTML);

This is all very well but storing the HTML is a bit crude and is problematic on FireFox for some un-investigated reason. Mark 2 will employ a more JSON-based approach, which will necessitate processing an HTML template to generate the HTML Task list from the JSON data.


Mark 2 – Storage and processing of JSON data

JSON is actually format the data structure (JavaScript Object) takes when converted into a string (JSON.stringify), which is a more convenient format for transport. To commence migration from HTML storage to JSON we first establish an HTML template we will use to generate the Task list from the JSON data. We replace the Task list wit the following HTML. Notice the use of double curly brackets as placeholders for the insertion of data.

<ol class="mainList" id="list_1">
  <li id="li_{{Id}}" class="lowLight">{{Task}}</li>
</ol>

As the application starts the LI element is copied and pre-processed to form a (global variable garrListTemplate) template as follows.

garrListTemplate = 
	objList_1.innerHTML.split(/(\{\{)|(\}\})/g);

We use the template to generate the Task list using the following call.

objList_1.innerHTML = generateTaskList();

But before we explore this any deeper there is some preparation to do, like getting the data in the first place. At this stage we are shamelessly going to use some global variables, which we will address in the next update (mark 3).

  • gnumMaxListItems: the Id of the last Task added to the list.
  • garrListTemplate: holds the HTML template as described above.
  • garrListData: is the object that contains the Task List data as an array.
    • This is the element that is persisted in the web local storage.

Also part of the start-up code is a call to a function to retrieve and prepare the Task list from Local storage.

garrListData = loadTaskList();

The loadTaskList is defined as follows and is intended to not only retrieve Task data from local storage but prepare it in a manner that ensures it is usable, even if empty, and offers the user a dummy data set if thing is retrieved.

function loadTaskList() {
 var strToDoList = localStorage.getItem("ToDoListJSON");
 var arrListData = {list:[]};

 if (!!strToDoList) {
   arrListData = JSON.parse(strToDoList);
 }
 if (!arrListData.list || !arrListData.list.length){
   if (confirm("Pre-populate?")) {
     arrListData.list = [
       {Id:1, Task:"Get out of bed"},
       {Id:2, Task:"Brush teeth"},
       {Id:3, Task:"Shower"},
       {Id:5, Task:"Grab breakfast"}
     ];
   }
   else {
     arrListData.list = [];
   }
 }
 return arrListData;
}

The function performs the following steps:

  1. Get the “ToDoListJSON” object, as a JSON string, from Local Storage, into the strToDoList local variable.
  2. Pre-define the list data object to contain an array called list.
  3. If strToDoList contains a string we use JSON.parse to try and convert it into the arrListData JavaScript object. However, there is a chance the data is not a valid object so we should not assume the object will be as required.
  4. If the list attribute is missing from the arrListData variable, or there is no data in the array, we prompt the user with the option to pre-populate the data object for test purposes.
    1. If they decline we ensure the object is defined correctly but empty.
  5. Finally we return the newly created object.

To convert the data into HTML, we use the generateTaskList function, as mentioned above and, as defined below.

function generateTaskList() {
 // Global: gnumMaxListItems - Next Item (Task) Index
 // Global: garrListTemplate - List Template
 // Global: garrListData - List Data
 var arrList = [];
 var arrItem = [];
 var numTask = 0;
 var numTmplt = 0;
 
 for (numTask=0; numTask<garrListData.list.length; 
	numTask+=1) {
   if (gnumMaxListItems < 
	garrListData.list[numTask].Id) {
     gnumMaxListItems = garrListData.list[numTask].Id;
   }
   for (numTmplt=0; numTmplt<garrListTemplate.length; 
	numTmplt+=1) {
     if (!!garrListTemplate[numTmplt]) {
       if (garrListTemplate[numTmplt] == "{{") {
         arrItem.push(
		garrListData.list[numTask][
			garrListTemplate[numTmplt+2]]);
         numTmplt+=4; //Skip over {{PLACEHOLDER}}
       }
       else {
         arrItem.push(garrListTemplate[numTmplt]);
       }
     }
   }
   arrList.push(arrItem.join(""));
   arrItem = [];
 }
 return arrList.join("\n");
}

This is a bit complicated but stick with it, I will try to go slow. We do make use of all three global variables (yuck but let’s continue) and declare local variable arrays to contain the generated HTML list and the fragments of each list item.

  1. We process through each item of data in the garrListData.list.
  2. We increase the value of gnumMaxListItem (from zero) when a higher Id is presented.
  3. We then process each fragment of the HTML Template we captured at the start of the program.
  4. As long as the fragment is valid (not undefined) we attempt to process it.
    1. If the fragment is “{{“, which denotes the start of a placeholder for a value,
      1. We add the value of data, referenced by the placeholder, to the Item array.
      2. We then skip the next 4 fragments as these contain remnants of the placeholder.
    2. If the fragment is anything else (HTML) we just add it to the Item array.
  5. The Item array is concatenated into a single string and added to the List array and cleared.
  6. Finally the List array is returned from the function as a multi-line string.

The output from the function is a block of HTML containing the an LI tag for each data item (Task).

The only other new function we have created for this edition of the application is findTaskItem which takes a single parameter pstrTaskLabel and is used to locate the data for a selected Task in the List.

function findTaskItem(pstrTaskLabel) {
  // Global: garrListData - List Data
  var numItemPosition = -1;
  var numItem;
  var numTaskId = Number(pstrTaskLabel.split("_")[1]);
 
  for (numItem=0; numItem<garrListData.list.length; 
	numItem+=1) {
    if (numTaskId == garrListData.list[numItem].Id) {
      numItemPosition = numItem;
    }
  }
  return numItemPosition;
}

The function passes through the list of Tasks (data) looking for a matching Id. If found the function returns the position in the array where the data can be found, if not the value -1 is returned. Remember, in JavaScript the index of arrays begin at zero not one.

All we have to do now is modify existing functions to update the Task data in response to user actions, although the HTML is be updated in the existing manner. All calls to store the data have to be changed to “stringify” (convert the JavaScript object to a JSON string) the Task data beforehand.

localStorage.setItem("ToDoListJSON", 
	JSON.stringify(garrListData));

New Tasks are added to the List data using a push on the to end of the array.

garrListData.list.push({Id: gnumMaxListItems, 
	Task:objTextArea.value});

Before updating or removing a Task from the List we first have to look up the index of the selected Task using the function defined above.

numTaskIndex = findTaskItem(objHighlightedItem.id);

We can then update the Task text,

garrListData.list[numTaskIndex].Task = objTextArea.value;

or remove the Task entirely, as required.

garrListData.list.splice(numTaskIndex,1);
localStorage.setItem("ToDoListJSON", 
	JSON.stringify(garrListData));

We are now managing the data in JSON format, making the application better prepared for Sprint 3, when we interact with the Web Server using AJAJ.

Now for some cleaning-up and removal of those nasty global variables.


Mark 3 – Implementation of a Model-View-Controller (MVC)

Establishing a Model-View-Controller (MVC) pattern is a good way of improving the maintainability of the source code. This achieved by separating data storage (Model) and presentation (View) concerns (code), and preserving isolation of those concerns through a Controller.

Model-View-Controller pattern

The MVC pattern is a widely adopted approach to managing the architecture of a UI development and can be found in well-respected frameworks such as JavaScriptMVCBackbone and Google’s AngularJS [Side note on the application of MVC in web applications]. In this edition of Sprint 2b the data will again be stored in Local Storage but will be the responsibility of the Model component [Side note on the role of the Model and data storage]. All presentation to the user will be performed by the View component and any interaction between the Model and the View will be conducted by the Controller to remove any inter-dependencies forming. The Controller will also be the recipient of all user events such as button clicks.

Installing an MVC is just one of the measures undertaken as part of the mark 3 task, to clean-up the code so far. As a result of incremental development (such as that employed in many Agile developments) the code can become messy and cumbersome. So, now is the time for a clean out and to start we will ensure greater code quality through the introduction of  the “use strict”; directive in all JavaScript files. This ensures all variables are declared (but not initialised) prior to use. The directive does not tighten data type restrictions so vigilance is required in this area.

The three components of the MVC are created in the same way, using the new command. This approach instantiates an extensible object that supports data hiding. All variables and functions declared within the function are private but all functions attached to the instance (as part of this or self) are exposed outside the function. As all the functions declared within a component are effectively “Closures” they retain access to the private variables, which are preserved through-out the execution of the application.

var gobjComponent = new function() { 
  var self = this;
};

Each component preserves reference to itself by storing this to the private variable self, which avoids ambiguity inside functions. By moving all code into one of the three major components the start-up code is reduced to this:

document.onreadystatechange = function () {
  if (document.readyState == 'complete') {
    gobjController.initialise(gobjModel, gobjView);
  }
};

When the document has completed loading and the HTML has been fully rendered, the Controller is initailised by passing it references to the Model and the View components. The initialise function of the Controller performs two functions:

  1. Capture a reference to the Model and View components.
  2. Call the initialise function of the Model and View components, passing them only a reference to the Controller (self).

The following fragment of code is the generic bases from which this edition of the application is formed.

/* MODEL object */
var gobjModel = new function() {
  var self = this;
  var controller;

  self.initialise = function( pobjController) {
    controller = pobjController;
  };
};

/* VIEW object */
var gobjView = new function() {
  var self = this;
  var controller;

  self.initialise = function( pobjController) {
    controller = pobjController;
  };
};

/* CONTROLLER object */
var gobjController = new function() {
  var self = this;
  var model;
  var view;

  self.initialise = function(pobjModel, pobjView) {
    model = pobjModel;
    view = pobjView;

    pobjModel.initialise(self);
    pobjView.initialise(self);
  };
};

The Model Component

The Model component manages the data regarding the list of Tasks, the last (highest) index used and the current selected Task. It is unaware of the User Interface and the View component. Any information the View component has, that the Model needs to obtain is request through the Controller. [Show Source Code]

Private Variables
var listData; The object containing the list of Tasks data.
var listIndex = 0; The index of the latest (highest value) Id in the list of Tasks.
var selectedTaskId; The Id of the selected Task (when applicable).
Private Functions
preserveTaskListData Stores the object containing the list of Tasks data to Local Storage as a JSON string.
restoreTaskListData Retrieves the object containing the list of Tasks data from Local Storage.
findTaskItem Locates and returns the position of the Selected Task in the array of stored tasks.
Public Functions
setListIndex Stores the Index of the latest Task at initialisation.
getTaskList Returns a copy of the current list of Tasks to the caller.
getSelectedTask Retrieves the Id of the selected Task from memory and returns it.
setSelectedTask Preserves the Id of the selected Task in memory.
updateTask Replaces the text of the selected Task for that supplied through the call and calls the preserverTaskListData function to store the change.
createTask Creates a new Task and adds it to the data set before calling preserverTaskListData to store the changes.
deleteTask Removes the selected Task from the data set and calls the preserverTaskListData function to store the changed data.
initialise This method is only called when starting the application. It retrieves data from localStorage but it the data is empty or malformed the function also offers the user opportunity to load test data instead.

The View Component

The View component manages the presentation of data to the User. The View retains references to on-screen components such as the container of the Task list and the field used by the user to enter the text of the Task. Similar to the Model component, the only other component the View has visibility of is the Controller. Any data obtained or required is send to/requested from the Controller, which will interact with the Model component on the View’s behalf. [Show Source Code]

Private Variables
var objTextArea; Reference to the control (Textarea field) used by the user to enter the Task text.
var objList; Reference to the control (Ordered List) used to present the list of Tasks to the user.
var taskTemplate; Object containing the fragments of HTML used to as a template for constructing the Task list entries (List Items).
Private Functions
generateTaskList Processes the supplied list of Tasks (data) and the task template to product the HTML for the Task list. Returns the HTML block along with the Task Index (highest Id).
getSelectedTask Identifies the Selected Task by it being highlighted within the list.
showFunctions Function used two swap between the Task Details and Button panels.
clearSelectedTask Clears the Task list of any selected task highlighting.
Public Functions
selectedItem Responds to the user clicking the Task List. It first confirms the item selected is a single Task before clearing currently any selected Task. The function then highlights the selected Task before copying its text value to the Task entry field.
clearSelectedTask Calls the private clearSelectedTask function to remove any selected Task from the Task list.
getTaskText Gets the value of the Task text (textarea) control and returns is to the caller.
setTaskText Sets the value of the Task text (textarea) control to the value specified.
showTaskDetails Places the calls required to a) show the panel containing the Task entry field and, b) sets focus to the main field (textarea).
showButtonPanel Places the calls required to a) show the panel containing the main buttons and, b) clears any selected Task in the task list.
createTask This function generates and new Task item in the on-screen list using the HTML DOM.
updateTask This function replaces the text of the selected task, as shown on-screen.
deleteTask This function removes the selected task from the Task list, as shown on-screen.
initialise This method is only called when starting the application. The function locates and records a reference to the Task List and Text Area on-screen components. It also extracts and pre-processes the Task item template before calling on the Controller to supply the Task Data. This function uses the Task Data to product the Task List HTML and obtain the Task index, by calling the generateTaskList function, before populating the list.

The Controller Component

The Controller orchestrates communication between the Model and View components and responds to User interaction (mouse clicks – buttons and list selections). [Show Source Code]

Private Variables

The Controller has not private variables in addition to the Controller (self) and references to the Model and View objects.

Private Functions

The Controller has not private functions at all.

Public Functions
taskSelect Task List on-click event handler: Calls the View to process and record the selected Task (List Item) before notifying the Model of the Selected Task’s Id.
getTaskList  Calls the Model to retrieve a copy of the current Task list data.
setTaskIndex  Calls the Model to set the Index of the selected Task.
editListItem Edit button event handler: First checks with the Model (getSelectedTask) to ensure a Task is selected before calling the View to show the Task Details panel.
addListItem Add button event handler: Clears all reference, in the View and Model components, to any selected Task. Also calls the View to show the Task Details panel.
saveListItem Save button event handler: Performs both the Update of changed Tasks and Insertion of new Tasks,
deleteListItem Delete button event handler: Confirms with the Model that a Task has been selected before confirming with the user to confirm the request to delete it. If confirmed the Model is called to remove the Task from the data set and the View is called to remove the Task from the presented list. Finally the View is called to switch the displayed panel from the Task Details to the Button panel.
cancelListItemChange Cancel button event handler: Calls for the View to show the Button panel.
initialise This method is only called when starting the application. It records a reference to the supplied Model and View components before initialising them.

Mark 4 – Decoupled event binding

Now we have an MVC pattern established there is another enhancement we can make. Currently the HTML includes Ids and links (onclick attributes) that facilitate the linkage of on-screen components to event handlers. I would argue this is the wrong way round. The HTML file can have reference points (like the Id attribute) the JavaScript can use to link event handlers but a) the HTML should not assume the existence of the handler or even know its name, and b) the linkage of event handlers to on-screen components should orchestrated from within the JavaScript. This is where the handlers reside and where connection can be controlled and automated; in much the same way as performed by AngularJS.

To implement this we first remove the onclick event attributes from the HTML and replace them with data-event attributes. The value of which need not be the name of a function but could be a reference that can be mapped to an event handler. We also replace the id attribute of both the Task list and the Textarea with a data-target attribute to look-up dynamically.

<main>
  <section>
    <ol class="mainList" data-target='TaskList' 
	data-event='taskSelect'>
      <li id="li_{{Id}}" class="lowLight">{{Task}}</li>
    </ol>
  </section>

  <section 
	class="function_Container show_list_Functions">
    <article class="list_Functions">
      <button data-event="deleteListItem">Delete</button>
      <button data-event="editListItem">Edit</button>
      <button data-event="addListItem">Add</button>
    </article>

    <article class="listItems_Functions">
      <textarea data-target='TaskTextArea' 
	placeholder="Add text here"></textarea><br/>
      <button data-event="saveListItem">Save</button>
      <button data-event="cancelListItemChange"
	>Cancel</button>
    </article>
  </section>
</main>

We then update the View initialisation function by first revising the mechanism used to locate the Task list and Textarea using the document.querySelector function as follows.

// Capture the TextArea control
objTextArea = document.querySelector(
	"*[data-target='TaskTextArea']");
// Capture the Task List control
objList = document.querySelector(
	"*[data-target='TaskList']");

We also add a mechanism to process all elements that have an on-click event we need to handle. For this we use the document.querySelectorAll function that locates all tags with a data-event attribute. For each node in the set returned we use its value to locate an appropriate method on the Controller. If none is found we flag it up as an exception, otherwise we attached the function to the located node on the on-click event.

// Bind event handlers 
// (Buttons and Task List : onclick events)
var arrEvents = document
	.querySelectorAll("*[data-event]");
var objEvent = {};
var strEvent;
 
for (var numEvent=0; numEvent<arrEvents.length; 
	numEvent+=1) {
  objEvent = arrEvents[numEvent];
  strEvent = objEvent.getAttribute("data-event");
  try {
    if (!!controller[strEvent]) {
      objEvent.onclick = controller[strEvent];
    }
    else {
      throw strEvent;
    }
  }
  catch(exc) {
    alert(
	"EXCEPTION: "+
	"Binding to an undefined event handler - ["+ 
	exc+ "]");
  }
}

AngularJS takes this approach much further and includes data attributes that wire controls up to the model but I feel this is taking things too far.

Now for Sprint Three.


Return to main section of this article.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s