In the previous task, we have refactored our todo app using a functional programming approach. By using closure, higher-order functions, and immutable data structures, we align with functional programming principles. In this section, we will explore an object-oriented approach to our code’s structure and organization.
In object-oriented programming, we model our application as a collection of objects that interact with each other. Each object has its own state and behavior. We can use classes to define the structure of our objects and create instances of these classes to represent individual objects.
In our case, we can define a Todo
class that represents a single todo item. Each todo item will have properties like id
, text
, completed
, and methods to toggle the completion status. We can also define a TodoList
class that represents a collection of todo items. This class will have methods to add, remove, and filter todo items.
Let’s start by defining the Todo
class. Create a new file named todo.js
in the src
directory and define the Todo
class as follows:
Todo
ClassThis syntax mush be familiar to you if you have experience with other object-oriented programming languages like Java or C++. We define a class named Todo
with a constructor method that initializes the object’s properties. We also define a toggle
method that toggles the completion status of the todo item.
Notice that we use the class
keyword to define a class in JavaScript. The constructor
method is a special method that is called when a new instance of the class is created. We can pass arguments to the constructor to initialize the object’s properties. In this case, we pass id
, text
, and completed
as arguments to the constructor.
The fields id
, text
, and completed
are defined as properties of the Todo
class using the this
keyword. We could have declared them inside the body of the class, but it is more idiomatic to define them in the constructor method. The completed
property has a default value of false
if no value is provided when creating a new instance of the Todo
class.
The toggle
method toggles the completed
property by negating its current value. Methods are declared using the method declaration syntax. (This syntax was explored in the chapter on JavaScript Functions). Inside a class declaration, unlike in an object literal, method definitions are not separated by a comma.
Todo
ObjectNow that we have defined the Todo
class, let’s create a new instance of the Todo
class and test the toggle
method. Add the following code to the todo.js
file right below the Todo
class definition:
Run the todo.js
file using Node.js to see the output. You should see the following output in the console:
We use the new
keyword to create a new instance of the Todo
class. We pass the id
and text
of the todo item as arguments to the constructor. Notice we do not explicitly call the constructor
method; it is called automatically when we create a new instance of the class.
We then call the toggle
method on the todo
object using the familiar dot notation. This method toggles the completion status of the todo item. We log the todo
object to the console before and after calling the toggle
method to see the updated completion status.
In JavaScript, class properties are public by default, which means they can be accessed and modified from outside the class. To see how this works, add the following code to the todo.js
file:
Run the todo.js
file using Node.js to see the output. You should see the following output in the console:
The ability to directly access and modify the properties of an object can be both a blessing and a curse. It provides flexibility but also exposes the internal state of the object. This can lead to unexpected behavior if the properties are modified directly without validation or error checking. In the next step, we will explore a technique to hide the internal state of an object from the outside world.
It is common in object-oriented programming languages like Java and C++ to hide the internal state of an object from the outside world. We can achieve this by making the properties of the object private and providing public methods to access and modify the properties. This concept is known as encapsulation.
The syntax for defining private properties in JavaScript is a new addition to the language. It involves prefixing the property name with a #
symbol. Private properties are only accessible within the class where they are defined. Let’s modify the Todo
class to make the id
property private:
Notice that we have prefixed the id
property with a #
symbol to make it private. We must also declare the #id
in the enclosing class scope to make it accessible within the class. If you try to run the code, you will see the #id
is hidden from the outside world:
As a reminder, this is our code where we instantiate a Todo
object and try to access the id
property:
Run the todo.js
file using Node.js to see the output. You should see the following outputs the console:
Let’s go over this output:
The Todo { text: 'Buy milk', completed: false }
output shows that the #id
property is hidden from the outside world. We instantiated the class and logged the object to the console, but the #id
property is not visible.
The Todo { text: 'Buy milk', completed: true }
output shows that the toggle
method works as expected. We called the toggle
method on the todo
object, and the completion status was toggled. The #id
property remains hidden.
The undefined
output says there is no id
property on the todo
object. Be careful here as we did not ask for the #id
property, but the id
property. The id
property is not defined on the todo
object, so it returns undefined
.
The Buy milk
output shows that the text
property is accessible from outside the class. The text
property is not private, so it can be accessed and modified directly.
The true
output shows that the completed
property is accessible from outside the class. The completed
property is not private, so it can be accessed and modified directly.
The Todo { text: 'Buy milk', completed: true, id: 2 }
output shows there is an id
property on the todo
object. The statement todo.id = 2;
creates a new id
property on the todo
object. So the #id
is not the same as the id
property! The #id
property is still 1
and remains hidden.
Let’s try something else. Add the following code to the todo.js
file:
Now run the todo.js
file using Node.js to see the output. You should see the following error in the console:
The error message indicates that the Node runtime thought we were trying to add a private field to the todo
object. This is not the case; we are trying to access the private field #id
. The error message is misleading, but it is a limitation of the current implementation of private fields in JavaScript.
If your code editor is equipped with a TypeScript linter, you may see a warning that says “Property ‘#id’ is not accessible outside class ‘Todo’ because it has a private identifier.” This is a more accurate description of the issue. The private field #id
is not accessible outside the class where it is defined.
If we were to define a getter method for the #id
property, we could access the private property indirectly. Add the following code to the Todo
class:
Now update the previous console log statement to use the getId
method:
Run the todo.js
file using Node.js to see the output. You should see the following output in the console:
In JavaScript class syntax, there is a special syntax for defining getter methods. A getter method is a method with no parameters that is declared with the get
keyword. Let’s redefine our getter method using this syntax:
Now update the todo.js
file to use the new getter method. However, before running the code, also comment out the todo.id = 2;
statement:
Run the todo.js
file using Node.js to see the output. You should see the following output in the console:
The 1
output is the value of #id
property accessed through the getter method. However, the way we call this getter method is different from calling a regular method. We access the id
property as if it were a regular property, but behind the scenes, the getter method is invoked to retrieve the value.
In the todo.js
file, if you uncomment the todo.id = 2;
statement and run the code, you will see the following error in the console:
Let’s update the Todo
class to hide other properties and provide getter and setter methods for them:
Run the todo.js
file using Node.js to see the output. You should see the following output in the console:
Let’s make the following observations:
The Todo {}
output shows that the #id
, #text
, and #completed
properties are hidden from the outside world. We instantiated the class and logged the object to the console, but the private properties are not visible.
The 1
output shows the value of the #id
property accessed through the getter method.
The commented out code todo.id = 2;
would throw an error because the id
property has only a getter method and no setter method.
The Buy milk
output shows the value of the #text
property accessed through the getter method. The statement todo.text = "Buy eggs";
updates the value of the #text
property using the setter method. Notice we defined a setter method for the text
property. The syntax for defining setter methods is similar to that of getter methods, but with the set
keyword. Like a getter method, a setter method is not called directly; it is invoked through assignment to the property.
The commented out code todo.completed = false;
would throw an error because the completed
property has only a getter method and no setter method. Instead, we call the toggle
method to toggle the completion status of the todo item. The printed false
and true
values show the completion status before and after calling the toggle
method. We access the completed
property through the getter method.
Please bear in mind that class syntax in JavaScript is a recent addition to the language. It is still evolving, and some features may not be fully supported in all environments. The private fields feature, in particular, is relatively new and may not be available in all JavaScript environments. Due to this immaturity, some developers prefer to not use private fields in JavaScript.
In object-oriented programming, static members are properties or methods that belong to the class itself rather than individual instances of the class. They are accessed using the class name rather than an instance of the class. Static members are useful for defining utility methods or properties that are shared across all instances of the class.
A good use case for static members in our Todo
class would be a method to generate unique IDs for todo items. We can define a static property to keep track of the last used ID. Let’s update the Todo
class to include these static members:
In this updated Todo
class, we define a static private property #nextId
to keep track of the next available ID for todo items. We initialize this property to 1
. In the constructor method, we assign the value of #nextId
to the #id
property of the todo item and then increment #nextId
by 1
.
When we create a new instance of the Todo
class, the #id
property is automatically assigned a unique ID based on the value of #nextId
. This ensures that each todo item has a unique (auto-incrementing) ID.
Todo
ClassTo use the Todo
class in other parts of our application, we need to export it from the todo.js
file. Update the todo.js
file to export the Todo
class:
Notice that I have deleted the code that instantiates the Todo
class and logs the output. We no longer need this code in the todo.js
file. We use the export default
syntax to export the Todo
class from the todo.js
file. This allows us to import the Todo
class in other files using the import
statement. For example, suppose we had a demo.js
file in the same directory as the todo.js
file. We could import the Todo
class in the demo.js
file as follows:
The import
and export
statements used in the previous step are recent additions to JavaScript known as ES6 modules. ES6 modules were introduced in ES6, or ECMAScript 2015, the sixth major version of the ECMAScript, a standard for scripting languages, including JavaScript, JScript, and ActionScript.
With ES6 modules, each file is treated as a separate module. This means that the variables, functions, classes, and other objects defined in a module are not available in the global scope. Instead, we can export and import functions, objects, or primitives from one module (file) to another. This allows for better encapsulation and separation of concerns.
export
: The export
statement is used to export values from a module so they can be used in other modules with the import
statement.
import
: The import
statement is used to bring in exports from another module. You specify the name of the exported item in curly braces { }
and the path to the module file.
You can mark one of the exported values as the default export from a module. This makes it easy to import in another file using syntax such as import Todo from './Todo'
.
Benefits:
Encapsulation: By using modules, we can hide the internal details of a module and expose only what’s necessary. This is a core concept in software design.
Namespace Management: Modules help in avoiding naming collisions in the global namespace. Since each module has its own scope, the same name can exist in different modules without any conflict.
Reusability: Modules can be reused across different parts of the application or even in different applications.
Note on Paths: When importing modules, the path can be relative or absolute. In our case, we’re using relative paths. The ./
at the beginning of the path indicates that the module is in the same directory as the current module.
TodoList
ClassNow that we have defined the Todo
class, let’s create a TodoList
class that represents a collection of todo items. Create a new file named todo-list.js
in the src
directory and define the TodoList
class as follows:
Let’s break down the code step by step:
We import the Todo
class from the todo.js
file. This allows us to use the Todo
class in the TodoList
class.
We define a constant FILTERS
object that contains three properties: ALL
, ACTIVE
, and COMPLETED
. These properties represent the different filters we can apply to the todo list.
We define the TodoList
class with a private field #todos
that stores the list of todo items. The #todos
field is initialized as an empty array.
We did not define a constructor method for the TodoList
class. The class uses the default constructor provided by JavaScript if no constructor is defined. Since the #todos
field is initialized as an empty array, we don’t need to perform any additional setup in the constructor.
We define several methods in the TodoList
class:
addTodo
: This method adds a new todo item to the list. It creates a new instance of the Todo
class with the provided text and pushes it to the #todos
array.
toggleTodoById
: This method toggles the completion status of a todo item with the specified ID. It finds the todo item in the list based on the ID and calls the toggle
method on the todo item.
getTodos
: This method returns a filtered list of todo items based on the specified filter. It uses the FILTERS
object to determine the filter criteria and returns the filtered list of todo items.
getNumberOfActiveTodos
: This method calculates the number of active (incomplete) todo items in the list. It uses the reduce
method to count the number of todo items with a completion status of false
.
deleteCompletedTodos
: This method removes completed todo items from the list. It filters out todo items with a completion status of true
and updates the #todos
array.
markAllCompleted
: This method marks all todo items as completed. It iterates over each todo item in the list and sets the completion status to true
.
We export the TodoList
class as the default export from the todo-list.js
file. This allows us to import the TodoList
class in other files using the import
statement.
Let’s make a few observations about the TodoList
class:
The #todos
field is a private field that stores the list of todo items. By making this field private, we encapsulate the internal state of the TodoList
class and prevent direct access to the list of todo items.
We mixed the practices of functional programming, structured programming, and object-oriented programming in the TodoList
class. The class uses methods like filter
, reduce
, and forEach
to manipulate the list of todo items. These methods are common in functional programming and are used to transform and process data in a declarative manner. On the other hand, we push directly to the #todos
array in the addTodo
method, and directly toggle the completion status of a todo item in the toggleTodoById
method. These actions are more characteristic of structured programming. The whole class is an example of object-oriented programming, where we define a class with properties and methods that operate on those properties.
We did not use the special getter method syntaxt with the getTodos
method. This is because the method takes a parameter (filter
) and performs a transformation on the data. Getter methods are typically used to retrieve a property value without any additional processing, and they cannot take parameters.
We used the export
keyword with the declaration of the FILTERS
object to make it available for import in other modules. The is an example of a non-default export, where we export a specific value from the module. If we were to import the FILTERS
object in another module, we would use the following syntax: import { FILTERS } from './todo-list.js';
.
TodoApp
ClassIn the main.js
file, we will define the TodoApp
class that represents the main application logic. The TodoApp
class will interact with the TodoList
class to manage the list of todo items and handle user interactions.
Let’s break down the code at a high level:
We import the TodoList
class and the FILTERS
object from the todo-list.js
file. This allows us to use the TodoList
class and the FILTERS
object in the TodoApp
class.
We define the TodoApp
class with private fields to store references to various elements in the HTML document. These fields represent the todo list, the current filter, the todo list element, the active todos count element, the todo navigation element, the input for new todos, the “mark all completed” button, and the “clear completed” button.
We instantiate the TodoApp()
at the bottom of the file. This creates a new instance of the TodoApp
class when the page loads.
Let’s break down the TodoApp
class in more detail:
The constructor
method initializes the TodoApp
class by setting up event listeners for user interactions and rendering the initial list of todos.
The #createTodoText
, #createTodoEditInput
, and #createTodoItem
methods are helper functions to create the HTML elements for displaying todo items. These methods are used to generate the necessary elements for each todo item.
The #renderTodos
method renders the list of todos based on the current filter. It clears the existing list of todos, generates new todo elements, and updates the active todos count.
The #handleKeyDownToCreateNewTodo
method is an event handler that creates a new todo item when the user presses the Enter key in the input field. It adds the new todo item to the list, clears the input field, and re-renders the list of todos.
The #findTargetTodoElement
and #parseTodoId
methods are helper functions to extract the todo ID from the target element when a user interacts with a todo item.
The #handleClickOnTodoList
method is an event handler that toggles the completion status of a todo item when the user clicks on the todo text. It updates the completion status of the todo item and re-renders the list of todos.
The #updateClassList
method is a helper function to update the class list of a navbar element based on whether it is active or not. The #renderTodoNavBar
method renders the navbar anchor elements based on the current filter selection.
The #handleClickOnNavbar
method is an event handler that filters the todos based on the navbar selection. It updates the filter, renders the navbar elements, and re-renders the list of todos.
The #handleMarkAllCompleted
method is an event handler that marks all todos as completed when the user clicks the “mark all completed” button. It updates the completion status of all todos and re-renders the list.
The #clearCompletedTodos
method is an event handler that clears all completed todos when the user clicks the “clear completed” button. It removes completed todos from the list and re-renders the list of todos.
Let’s make a few observations about the TodoApp
class:
The fields and methods of the TodoApp
class are all private, indicated by the #
symbol. This encapsulates the internal state and behavior of the class and prevents direct access to these elements from outside the class. Once the app is instantiated, the user interacts with the app through the event listeners and the rendered HTML elements.
The TodoApp
class combines the logic for managing the todo list, handling user interactions, and rendering the UI. This is a common pattern in front-end development, where the logic and presentation are closely tied together. The class uses event listeners to respond to user actions and updates the UI based on those actions.
The TodoApp
class uses helper functions to create and manipulate HTML elements. This separation of concerns helps keep the code organized and makes it easier to maintain and extend the application. The code modularization is very similar to what we ended up with in the previous task where we implemented this app in functional programming style.
In several cases, we use the bind(this)
method to bind the context of the event handlers to the TodoApp
instance. This ensures that the event handlers have access to the private fields and methods of the class. The bind
method creates a new function that, when called, has its this
keyword set to the provided value.
Let’s elaborate on the last point. Consider this code snippet from the TodoApp
class:
The handleKeyDownToCreateNewTodo
is invoked when you press a key down. The handleKeyDownToCreateNewTodo
uses the this
keyword to access the todoList
and call the renderTodos
method.
However, when the handleKeyDownToCreateNewTodo
is called by addEventListener
, its execution context is not the TodoApp
anymore. Instead, it is the execution context of addEventListener
which in this case is the inputNewTodo
element. Therefore, we need to explicitly bind its this
keyword to the TodoApp
when passing it as an argument to addEventListener
. This point is much harder to figure out on your own if I had not told you about it 😜.
Alternatively, if you declare the handleNewTodoKeyDown
as an arrow function like this:
Then you don’t need to bind its this
keyword! (Think why?)
In this task, we implemented a simple todo list application using object-oriented programming principles in JavaScript. We defined the Todo
class to represent individual todo items, the TodoList
class to manage a collection of todo items, and the TodoApp
class to handle user interactions and render the UI.