Component Based Design - Part 2 - Data Access Components
Before starting this activity you should understand the concepts discussed in this article.
The components that we have experimented with so far have been UI components (user interface), which means that they are components that users can interact with, by filling out forms and clicking on buttons.
There are other types of components in an application that are not seen, but they provide some sort of functionality. You might have a 'messaging' component that allows the system/application to send texts and emails. You might have a component that logs the activity takes place within the system.
Data access components are used to manage the information stored by a system. In many business applications, information is stored in a relational (SQL) database, but there are other ways of storing information.
A data access component allows the application to interact with some type of database, and it usually includes methods/functions that allow for CRUD operations. CRUD stands for create, read, update, and delete. In SQL terms it would be equivalent to INSERT, SELECT, UPDATE, and DELETE queries.
For this exercise, we'll build a data access component that does CRUD operations for a collection of 'user' objects. To keep things simple, we'll use the browser's localStorage database.
In a real-world app, you probablly wouldn't use localStorage because of it's drawbacks (we can discuss them in class). But localStorage is great for prototyping apps because you don't have to take the time to design a SQL database (and writing all the complicated queries to interact with it).
Start by creating a folder named user-data-access and put it inside the component-based-design folder that you created in the previous activity. You can put all the files we create throughout this activity in the user-data-access folder.
Then create a file named user-data-access.js and put the following code in it:
class UserDataAccess{
///////////////////////////////////////////////
// PRIVATE INSTANCE VARIABLES (start with #)
///////////////////////////////////////////////
// We'll use the dummyData to populate the localStorage database
#dummyData = [
{id:1, firstName:"Jane", lastName:"Doe", email:"jdoe@acme.com"},
{id:2, firstName:"Tony", lastName:"Thompsom", email:"tony@acme.com"},
{id:3, firstName:"Jesse", lastName:"Jones", email:"jesse@acme.com"}
];
//////////////////////////////////
// CONSTRUCTOR
//////////////////////////////////
constructor(){
// check to see if 'userData' key is already in the localStorage database
// if not, then create it, and populate it with the dummy data
if(!localStorage.getItem("userData")){
localStorage.setItem("userData", JSON.stringify(this.#dummyData));
}
}
//////////////////////////////////
// PUBLIC METHODS
//////////////////////////////////
//////////////////////////////////
// PRIVATE METHODS (start with #)
//////////////////////////////////
}
In this case we've used a class to create our component. Right now, the class has a private instance variable (#dummyData) and a constructor (we'll add more to it soon). The dummyData variable just provides some initial data to load into the browser's localStorage. The code in the constructor function checks to see if a key named 'userData' exists in the localStorage database. If this key does not already exist, then it is created and populated with the dummyData.
Now we'll create a web page to test out our data access component. Create a file named user-data-access-tests.html and put this code in it:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>User Data Access Tests</title>
<script src="user-data-access.js"></script>
<script>
const uda = new UserDataAccess();
</script>
</head>
<body>
</body>
</html>
Notice that our data access class is 'imported' in the first SCRIPT element. The code in the second SCRIPT element calls the constructor to create an instance of the component, which is stored in the uda variable.
Load the .html page in the browser and then use the Developer Tools to inspect the localStorage. To do this, press F12 to open the Developer Tools, then click on the Application tab (note that some keyboards require you to hold down the 'fn' key while pressing F12). From there you should be able to see the inital (dummy) data assigned to the 'userData' key.
Now we'll add some public methods to the UserDataAccess class that will allow us to interact with the data. The public methods of a class are known as the API (application programming interface). Developers who use this class will instantiate it and then call the public methods to interact (interface) with it.
Let's start by adding a method that fetches all the users, add this method to the UserDataAccess class (under the comment for PUBLIC METHODS):
getAllUsers(){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
return users;
}
Make sure you understand all three lines of code (we'll be seeing them over and over again throughout the activity). If you have any question about any of it, I beg you to come and ask me about them. The first line of code gets the data stored in the 'userData' key of the localStorage database. Key/value databases can only store strings, but we can convert the string into an array of users by calling the parse() method of the JSON API. The final line returns the array of users.
Next we'll test out this method by calling it from the .html file. Add this code just, under the call to the constructor in user-data-access-tests.html:
// Test getAllUsers()
console.log(uda.getAllUsers());
If you reload the page and look at the console log, you should see the intial data. If so, then it appears that our first test passes by successfully fetching all users in the database.
Now we'll add a method that gets a user by their id. Add this method to the UserDataAccess class (put it under the getAllUsers() method):
getUserById(id){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
const user = users.find((u) => u.id == id);
return user;
}
Test this method by adding the following code to the .html file (just after the previous test):
// Test getUserById()
console.log(uda.getUserById(3));
When you reload the page, you should see the information for Jesse Jones in the console log.
Now we'll add a method to create a new user. But before we do, we'll add a private method that can be used to assign IDs to users when they are inserted. If we were using a SQL database we would not have to assign IDs when users are inserted because it would be done by the database. But a localStorage database is just a simple key/value store that doesn't do much, so we'll need to generate unique IDs when users are inserted.
First we'll add a private method named #getMaxId(). The # before the method name indicates that it is private, and can only be called from within the UserDataAccess class (it's for internal use only). This method gets the users array from localStorage and then and then returns the highest id in the array.
Add this code under the comment for PRIVATE METHODS (in user-data-access.js):
#getMaxId(){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
let maxId = 0;
for(let x = 0; x < users.length; x++){
if(users[x].id > maxId){
maxId = users[x].id;
}
}
return maxId;
}
And now add the following method to the UserDataAccess class (in the PUBLIC METHODS section):
insertUser(newUser){
// We really should validate newUser before inserting it!
// Set the new user's id:
newUser.id = this.#getMaxId() + 1;
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
users.push(newUser);
localStorage.setItem("userData", JSON.stringify(users));
}
Note that this method has a parameter for the new user object, this object will be added to the database.
In the body of the method, we first generate a unique ID by calling the private method added in the previous step. We then pull the data from localStorage and convert the string into an array of user objects (remember that key/value databases can only store strings, but we can convert strings into arrays and objects with the JSON API). Then we push the new user onto the array, and finally save the changes to localStorage.
If you are having trouble understanding this code, please ask me about it.
Also note the comment about validating the new user before inserting it. We should really make sure that the newUser object contains valid data, so that we don't corrupt the database. But we'll skip the validation code for now, just to keep things simple.
To test the insertUser() method, add this code to the .html page:
// Test insertUser()
uda.insertUser({firstName:"Marty", lastName:"Tanner", email:"marty@acme.com"});
uda.insertUser({firstName:"Phen", lastName:"Van", email:"phen@acme.com"});
console.log(uda.getAllUsers());
If you refresh the page, you should see the console log messages that include Marty and Phen. Make sure to check the IDs that were assigned to each user, this will indicate to us that the #getMaxId() method is working properly.
Now refresh the page, and you'll see that a duplicates of Marty and Phen have appear. Each time you refresh the page, another duplicate of Marty and Phen is added to localStorage. To prevent this, we can clear out the localStorage before running our tests. Add this code before the call to the constructor function in user-data-access-tests.html, it simply wipes our your browsers localStorage database:
//Wipe out the database before testing:
localStorage.clear();
Now we'll add a method that allows us to update a user in the database. Add this method to the UserDataAccess class:
updateUser(updatedUser){
// again, we should validate updatedUser before putting it in the database
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
const indexOfUserToUpdate = users.findIndex(u => updatedUser.id == u.id);
users[indexOfUserToUpdate] = updatedUser;
localStorage.setItem("userData", JSON.stringify(users));
}
See if you can follow each line of code in the body of the method. If you don't understand anything, please, let me know. After fetching the data and parsing it into an array of user objects, it uses the findIndex array method to find the position of the user object that needs to be updated. It then replaces that object with the one passed in as a parameter.
To test the method, add this to the .html page:
// Test updateUser()
const marty = uda.getUserById(4); // first, get a user to update
marty.lastName = "THOMPSON"; // make an update
uda.updateUser(marty);
console.log(uda.getAllUsers());
When you refresh the page, the last console log entry should show that the change has been saved to the database.
Let's add a method that deletes a user, add this code to the UserDataAccess class:
deleteUser(id){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
const indexOfUserToRemove = users.findIndex(u => id == u.id);
users.splice(indexOfUserToRemove,1);
localStorage.setItem("userData", JSON.stringify(users));
}
The method takes a parameter that represents the id of the user to delete.
Add this code to the .html page to test out the deleteUser() method:
// Test deleteUser()
uda.deleteUser(4);
console.log(uda.getAllUsers());
When you refresh the page, the final console log should show that Marty has been removed from the database.
What we have just done is created a data access component that interacts with users stored in a localStorage database. The component allows us to do CRUD operations with the users. Again CRUD stands for 'create', 'read', 'update', and 'delete'.
Afficionados will note that there are some problems with the way this class is designed. It breaks the DRY rule (do not repeat yourself) by repeating all the code that pulls the string from localStorage and parses it into an array of users. Instead of duplicating this code, we should really add another private method that includes these two lines in the body. And call the method instead of duplicating the code.
We are also duplicating the string 'userData' in many places. It would be better to use a constant instead. Then, instead of hard-coding 'userData' all over the place, like we are now, we could refer to the constant (it would make it very easy to change the key name if we needed to). Please ask if you have any questions about this.