First, let’s create content that is need for our application by creating the HTML framework and then adding the static view components.
Let’s first create a blank framework and tweak it for mobile devices if necessary.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>e-ToDo List</title>
</head>
<body>
</body>
</html>
Now that we have a framework established, let’s add static view components to it.
<body> <div> <h1>e-ToDo List</h1>NOTE: This is standard HTML code. There is nothing dynamic here.
<form>
<input type="text" size="30" placeholder="Add New Item">
<input type="submit" value="ADD">
<fieldset style="width:230px">
<legend>My To Do List</legend>
<div>
<input type="checkbox"> <span>List Item</span>
</div>
</fieldset>
</form>
<p><button>Remove Checked Item(s)</button></p> </div> </body>
Now that we have created the basic HTML content, let's "style" the application with some CSS goodness.
body {
font-family: Arial;
}
/* Panel styles ---------------------------------------*/
.panel {
width: 265px;
background-color:antiquewhite;
margin: 10px;
padding: 20px;
-webkit-box-shadow: 5px 5px 5px gray;
box-shadow: 5px 5px 5px gray;
border-radius: 10px;
}
/* Fieeldset style -------------------------------- */
fieldset{border: 1px solid blue;}
/* App Title styles -------------------------------- */
.app_title{
text-align: center;
border-radius: 25px;
background-color:blue;
color: white;
border: 1px solid gray;
}
<link href="e-ToDoList.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="panel">
<h1 class="app_title">e-ToDo List</h1>
<form>
<input type="text" size="30" placeholder="Add New Item">
<input type="submit" value="ADD">
<fieldset style="width:230px">
<legend>My To Do List</legend>
<div>
<input type="checkbox"> <span>List Item</span>
</div>
</fieldset>
</form>
<p><button>Remove Checked Item(s)</button></p>
</div>
Now that we have the standard framework in place, let’s add some dynamic content.
We will start first by:
// JavaScript Document
var app = angular.module("eToDoListApp", []);
<!-- AngularJS Scripts ---------------------------------------------- --> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<script type="text/javascript" src="e-ToDoList.js"></script>
</body>
CODE EXPLANATION:
- It is important that the AngularJS framework script is loaded FIRST before the script
that uses it. Otherwise, there will not be a reference to the framework and an error will
be throw
<body data-ng-app="eToDoListApp">
Now that we have created an app, let’s associate it with a controller to “control” our app. We want to add the following functionality to our controller:
After entering a "to do" item:
Write the following highlighted code:
WHY: To assign a controller to the app and to give it a variable name of app_title.
var app=angular.module("eToDoListApp", []);
app.controller("eToDoListCtrl", function($scope) {
"use strict"; $scope.app_title ="e-ToDoList";
}); // End of Controller --------------------------CODE EXPLANATION:
- There are two arguments that are passed to the controller. The first is the name of the
controller (eToDoListCtrl) and the second is the function (or how the controller will be used).
- The "use strict" statement is used to make the code more robust.
- The $scope.app_title variable will be used shortly to dynamically create the title of the app.
- NOTE: You may see an error in Dreamweaver that "angular in not defined." The reason for this
is because this script is written OUTSIDE of the app page and reference the AngularJS
framework script which is not included in this file. However, this is not an issue
when the app load because the the framework script will be loaded first and then the
other script will be loaded so it will have a reference to the AngularJS framework.
You will see this error in the file several times.
Now that we have associated the controller with the app within the SCRIPT code, we now need to associate the SCRIPT code within the HTML code.
<body data-ng-app="eToDoListApp" data-ng-controller="eToDoListCtrl">
<div class="panel">
<h1 class="app_title">{{app_title}}</h1>
<form>
<script>
var app=angular.module('eToDoListApp', []);
app.controller("eToDoListCtrl", function($scope) {
// Default Item -------------------------------------------------
$scope.todoList=[{todoText:'Gas car', done:false},{todoText:'Pick up kid', done:false}];
});
</script>
CODE EXPLANATION:
The $scope.todoList variable is an array with two objects in it with:
- a todoText property set to an initial value of "Gas car" and "Pick up kid"
- a done property set to an initial Boolean value of false for both objects
<form>
<input type="text" size="30" placeholder="Add New Item">
<input type="submit" value="ADD">
<fieldset style="width:230px">
<legend>My To Do List</legend>
<div data-ng-repeat="x in todoList">
<input type="checkbox" data-ng-model="x.done"> <span class="done-{{x.done}}">{{x.todoText}}</span>
</div>
</fieldset>
</form>
CODE EXPLANATION:
- The data-ng-repeat directive is used to iterate through the todoList array.
- The data-ng-model directive is used to "model" the done property of the
todoList array object.
Now that we can see some default values displayed, let’s create an "add item" function so that if a user click the ADD button it will add the "to do" item to the list.
<script>
var app=angular.module("eToDoListApp", []);
app.controller("eToDoListCtrl", function($scope) {
// Default Item ------------------------------------------------- $scope.todoList=[{todoText:'Gas car', done:false},{todoText:'Pick up kid', done:false}];
// Add function -------------------------------------------------
$scope.todoAdd=function() {
$scope.todoList.push({todoText:$scope.todoInput, done:false});
$scope.todoInput="";
};
});
</script>
CODE EXPLANATION:
- The push() method of the todoList array is used to ADD a "to do" item to the
array when the ADD button is clicked with a todoText property that will be
equal to the what is typed in the input field and a done property with its
initial value set to false.
- The $scope.todoInput="" is used to CLEAR the input field once the the
input field value has been added to the array.
<form data-ng-submit="todoAdd()">
<input type="text" data-ng-model="todoInput" size="30" placeholder="Add New Item">
<input type="submit" value="ADD">
<fieldset style="width:230px">
<legend>My To Do List</legend>
<div data-ng-repeat="x in todoList">
<input type="checkbox" data-ng-model="x.done"> <span data-ng-bind="x.todoText"></span>
</div>
</fieldset>
</form>
CODE EXPLANATION:
- The data-ng-submit directive is used to submit the form when the ADD button
is clicked and call (invoke) the todoAdd() method.
- The data-ng-model directive is used to "model" the data that is typed into
the input text field.
Now that we can view and add items to the list, let's add code to remove item(s) from the list.
<script>
var app=angular.module('eToDoApp', []);
app.controller("eToDoListCtrl", function($scope) {
// Default Item -------------------------------------------------
$scope.todoList=[{todoText:'Gas car', done:false},{todoText:'Pick up kid', done:false}];
// Add function -------------------------------------------------
$scope.todoAdd=function() {
$scope.todoList.push({todoText:$scope.todoInput, done:false});
$scope.todoInput="";
};
// Remove function ---------------------------------------------
$scope.remove=function() {
var oldList=$scope.todoList;
$scope.todoList=[];
angular.forEach(oldList, function(x) {
if (!x.done) {
$scope.todoList.push(x);
}
});
};
});
</script>
CODE EXPLANATION:
This function is used to:
- Create a new array named oldList based on the initial array (todoList) to hold
its data.
- Clear existing array (todoList)
- Then loop through the old array (oldList) and add BACK to the existing array
(todoList) ONLY the element(s) that has NOT been checked.
- The !x.done condition in the "if" statement is checking to see if the done
property is NOT (FALSE) or TRUE.
NOTE: When you select a checkbox to select it, you set the done property to true
because of the ng-model directive.
ANALOGY: The above function is akin to taking all of the cookies of the cookie jar
and placing them in another cookie jar, then eating the cookies you like (ones
that has been compledted) and placing the untouched cookies back in the original jar.
<p><button data-ng-click="remove()">Remove Checked Item(s)</button></p>
There are several enhancements that can be added to improve and make the app look better:
Currently, if you press the ADD button WITHOUT entering text in the text input field, an EMPTY todo item will STILL be created with a checkbox next to it. To prevent this from happening, we will rewrite the add function in an "if/else" statement and check to see if text is entered into the text input field and if not do nothing; otherwise, perform the normal add function.
// Add function -------------------------------------
$scope.todoAdd=function() {
if($scope.todoInput.length === 0)
{
// Do nothing
}
else
{
$scope.todoList.push({todoText:$scope.todoInput, done:false});
$scope.todoInput = "";
}
};
CODE EXPLANATION:
- The length property of the todoInput text field will return the number of characters, if any.
While optional, it would be nice to have an item that is selected shown with a strike through and in a red font to mimic a traditional paper todo list.
/* App Title styles -------------------------------- */
.app_title{
text-align: center;
border-radius: 25px;
background-color:blue;
color: white;
border: 1px solid gray;
}
/* Input item styles ---------------------------------*/
.done-true {
text-decoration: line-through;
color: red;
}
<div data-ng-repeat="x in todoList">
<input type="checkbox" ng-model="x.done"> <span data-ng-bind="x.todoText" class="done-{{x.done}}"></span>
</div>
CODE EXPLANATION:
- If the class resolved to "done-true," that class will be added to the element
to show a strike through and see the font color to red.
While also optional, it would be nice to let the user know how many items that have remaining to be completed when an item is added or deleted from list.
</form>
<p class="remain_display">{{remaining()}} of {{todoList.length}} list items remaining to be completed</p>
CODE EXPLANATION:
- The remaining() function will RETURN a value from the function. Normally, a
function performs a series a steps; however, when you want it to return a value,
the key word "return" is used as you will see shortly.
/* Input item styles ---------------------------------*/
.done-true {
text-decoration: line-through;
color: red;
}
/* Remaining Items styles --------------------------- */
.remain_display {
text-align: center;
background-color: white;
font-size: 12px;
display: block;
border: 1px solid gray;
padding: 0 4px;
}
// Remove Function ---------------------------------
$scope.remove=function() {
var oldList = $scope.todoList;
$scope.todoList = [];
angular.forEach(oldList, function(x) {
if (!x.done) {
$scope.todoList.push(x);
}
});
};
// Remaining Function ------------------------------
$scope.remaining = function() {
var count = 0;
angular.forEach($scope.todoList, function(todo){
count += todo.done ? 0 : 1;
});
return count;
};
}); // End of Controller --------------------------
CODE EXPLANATION:The ternary "if" statement (count += todo.done ? 0 : 1;) could also be REWRITTEN as a tradition "if" statement
- The initial value of the count variable is set to zero.
- The angular.forEach() method is used to loop the array (todoList) and run a function for each object
in that array against their done property to ascertain if they are true or false.
// Remove Function ---------------------------------
$scope.remove=function() {
var oldList = $scope.todoList;
$scope.todoList = [];
angular.forEach(oldList, function(x) {
if (!x.done) {
$scope.todoList.push(x);
}
});
};
// Remaining Function ------------------------------
$scope.remaining = function() {
var count = 0;
angular.forEach($scope.todoList, function(todo){ if(todo.done) {
count += 0; } else {
count += 1; }
});
return count;
};
}); // End of Controller --------------------------
CODE EXPLANATION:
- The conditional statement todo.done is the shortcut for todo.done == true. Adding "== true" is not
necessary because it is IMPLIED to be true by default. For beginner developers, you may want to add
"== true" to the conditional statement because it makes it easier to understand.
- The remaining function will set a count variable to zero initially. Then, loop through the todoList
array for the number of elements in the array and if the done property is true for a given object, don't
add to the count (count + 0); otherwise if the done property is false (meaning a list item is not clicked),
add one to the count (count + 1). In essence what we are doing is "skipping" over any to do item that is
completed.
Currently, the app will work as expected. However, if you refresh the browser, the app will show its default state. To make the data persistent, we will add Local Storage so that if a user exit the browser and then open it back again, the data will still be available.
See Add Local Storage for details on how to implement Local Storage.
In order to save the data locally, we will need to create a local storage object. Then, when the page loads, check the local storage and load it or use the default data if no local storage exists.
// Default Item ------------------------------------------------- $scope.app_title = "e-ToDo List";
$scope.saved = localStorage.getItem("todoLS");
if ($scope.saved !== null) {
alert("Save data to local storage");
$scope.todoList = JSON.parse($scope.saved);
}
else
{
alert("Use default data");
$scope.todoList=[{todoText:'Gas car', done:false}, {todoText:'Pick up kid', done:false}];
}
localStorage.setItem("todoLS", JSON.stringify($scope.todoList));
// Add function -------------------------------------------------
CODE EXPLANATION:
- The saved variable is used to get the saved data if there is any.
- If the saved variable is NOT NULL, the "if" portion of the conditional statement will be executed
which will result in data being saved to the Local Storage.
- If the saved variable is NULL, the "else" portion of the conditional statement will be executed
which will result in the default data being loaded from the array.
- The setItem() method is used to save the data to the Local Storage.
- The stringify() method of the JSON object is used to convert the todoList array data that is
passed as an argument to a "string"
// Default Item -------------------------------------------------
$scope.saved = localStorage.getItem("todoLS");
if ($scope.saved !== null) {
// alert("Save data to local storage");
$scope.todoList = JSON.parse($scope.saved);
}
else
{
// alert("Use default data");
$scope.todoList=[{todoText:'Gas car', done:false}, {todoText:'Pick up kid', done:false}];
}
localStorage.setItem("todoLS", JSON.stringify($scope.todoList));
// Add function -------------------------------------------------
// Add function -------------------------------------------------
$scope.todoAdd=function() {
if($scope.todoInput.length === 0)
{
// Do nothing
}
else
{
$scope.todoList.push({todoText:$scope.todoInput, done:false});
$scope.todoInput = "";
localStorage.setItem("todoLS", JSON.stringify($scope.todoList));
}
};
// Remove function ---------------------------------------------
$scope.remove=function() {
var oldList=$scope.todoList;
$scope.todoList=[];
angular.forEach(oldList, function(x) {
if (!x.done) {
$scope.todoList.push(x);
}
localStorage.setItem("todoLS", JSON.stringify($scope.todoList));
});
};
// Remaining Function ------------------------------------------
Extract from: https://docs.angularjs.org/error/ngRepeat/dupes
Error: ngRepeat:dupes
Duplicate key in a repeater are not allowed because AngularJS uses keys to associate DOM nodes with items. By default, collections are keyed by reference which is desirable for most common models but can be problematic for primitive types that are interned (share references).
SOLUTION: To resolve this error either ensure that the items in the collection have unique identity or use the track by syntax to specify how to track the association between models and DOM.
Use 'track by' expression (e.g., track by $index) to specify unique keys which will cause the items to be keyed by their position in the array instead of their value.
<div data-ng-repeat="x in todoList track by $index">
<input type="checkbox" data-ng-model="x.done"> <span data-ng-bind="x.todoText" class="done-{{x.done}}">List Item</span>
</div>
Currently, the application is working almost pefectly. However, if you check one or more "todo" items and then reload the page, the strikethrough feature does not persist. This will be resolved in the following steps.
<div data-ng-repeat="x in todoList track by $index">
<input data-ng-click="updateDoneStatus($index)" type="checkbox" data-ng-model="x.done">
<span class="done-{{x.done}}">{{x.todoText}}</span>
</div>
// Update done status in array -------------------------CODE EXPLANATION:
$scope.updateDoneStatus = function(id) {
if($scope.todoList.done)
{
$scope.todoList[id].done !== $scope.todoList[id].done;
}
localStorage.setItem("todoLS", JSON.stringify($scope.todoList));
};
}); // End of Controller ----------------------------