Introduction
In this part of the series, we will apply the principles of object oriented programming (oop) to re-create the star-rating widget we saw in part 1 and make it reusable in the process.
This is a common frontend engineering interview question.
Part 1 of this series can be found here.
The widget we built in part 1 was not reusable. The widget we will build in this article should be flexible to be used as many times as one wants whether on the same page or inside any part of an application. All you need to have is an HTML div
element, with a unique class attribute, that is going to serve as a container for the widget wherever it gets inserted.
To reach our goal, we will create a Javascript class from which widget instances will be created.
Setup
The setup will be the same as it was in the first article. We will use the JS Template from codesandbox.io. This template comes with a src
folder that containsindex.js
,style.css
files and an index.html
file outside of src
. This time we won't need to add any styles in the styles.css file.
HTML
The HTML part is very simple. Only one line needs to be changed. Go into the index.html
file and replace the existing div
with the following line.
<div class="stars-container1"></div>
This div
will serve as a container for the stars widget we are going to create later.
Javascript
Since we are using object oriented programming to create the widget, let’s go ahead and create the skeleton of our class inside the index.js
file.
class Stars {
constructor() {}
init() {}
setRating() {}
}
This class named Stars
and, currently, has empty constructor
, init
and setRating
methods.
Constructor
The constructor
will help us build unique widget instances. We will make it accept parameters that determine:
where the widget will be placed
the number of stars it will have
styles for each list item element which will serve as container for an individual star
The className
, numOfStars
and styleOptions
parameters will have an empty string, 0 and an object literal with a margin
attribute as default values respectively.
constructor(
className = "",
numOfStars = 0,
styleOptions = { margin: "5px" }
) {
this.isValid = false;
this.numOfStars = numOfStars;
this.stars = [];
this.styleOptions = styleOptions;
this.starsContainer = document.querySelector(className);
try {
if (this.starsContainer) {
this.className = className;
this.isValid = true;
} else {
this.isValid = false;
throw new Error(`${className} does not exist.`);
}
} catch (e) {
console.log(e.message);
}
if (this.isValid && this.numOfStars > 0) {
this.init();
}
}
In the above, you can see that we are adding several attributes on the this
object. The variable this.isValid
will serve as a flag to indicate the parameters that get passed into the constructor are valid. It is important to do validation at this stage to ensure proper values are passed into the constructor
at the time the widget is being built.
We also need to ensure the className
that gets passed into the constructor is a valid class name of an exiting element in the index.html
file. This is important because that element will be the one on which we will mount the widget. In addition, if the user passed in 0
as the number of stars, there won't be any widget. Therefore, if we don’t have valid values for the className
and numOfStars
parameters, the value of this.isValid
will be false
and that will save us from invoking the init
function unnecessarily.
We also have a this.stars
variable that has an empty array as its value and this.starsContainer
whose value is the returned value of querySelector
. Both will play important roles shortly.
Exception handling
A try-catch
block is added to to catch exceptions that could arise due to invalid classNames
. Inside the try block, we check whether or not the value of this.starsContainer
exists. If it does exist, we assign the className
to this.className
and the value true
to this.isValid
. If that is not the case, we assign the value false
to this.isValid
and throw an error using the built in Javascript Error
constructor. You can pass into this Error
constructor any message that you think will be properly express the issue. This is a good approach for creating generic errors.
When an error is thrown, our program execution will jump into the catch
block. Inside the catch
we can open an error modal to show the error message. But, for this article, I will just print the error message on the console.
Outside of the try-catch
block, the last piece of code we write is a condition that checks if the value of this.isValid
is true and the value of this.numOfStars
is greater than 0
. And, if that is the case, we can invoke theinit
method.
Init
The init
method is there to abstract away from the constructor logic we need to:
create an empty unorderd list (
ul
) elementgenerate list items and anchor elements
add star HTML entities to the anchor elements
add click event listeners to the anchor elements and implement the logic that will change the colours of the stars
append each anchor element to its respective list item element
append each list item element to the
ul
element we createdappend the
ul
element to the thestarsContainer
element
init() {
const ul = document.createElement("ul");
for (let i = 0; i < this.numOfStars; i++) {
this.stars.push({ id: i + 1 });
}
ul.style.listStyleType = "none";
ul.style.display = "flex";
const stars = this.stars.map((star) => {
const li = document.createElement("li");
const a = document.createElement("a");
li.style.margin = this.styleOptions.margin;
a.style.cursor = "pointer";
a.innerHTML = "★";
a.id = star.id;
a.addEventListener("click", (e) => {
this.setRating(ul, e);
});
li.appendChild(a);
return li;
});
const fragment = document.createDocumentFragment();
for (const star of stars) {
fragment.appendChild(star);
}
ul.appendChild(fragment);
this.starsContainer.appendChild(ul);
}
You can see the ul
element is created using the createElement
method of the document
object. We also have a for
loop which lets us push into the this.stars
array an object literal with an id
attribute and a value equal to the current iteration variable for the loop. Once that is done, we apply styles to remove list item bullet points and horizontally align the list items we are going to create shortly.
Now, we have an array of objects stored inside the this.stars
variable. We will use this array to generate li
elements which in number will be equivalent to the value stored in this.numOfElements
. To do that we need to invoke map on the this.stars
array and transform each object that we have in the array into the a li
element that wraps an anchor element with a star HTML entity
.
Right away, inside the callback function, we create two elements li
and a
and apply some styles to both. In addition, we set an HTML entity
which will appear as a star when rendered on the browser to the innerHTML
attribute of the anchor element.
We also invoke the addEventListener
method on the anchor element. This will help us listen to click
events, whenever the element is clicked. The logic we apply inside the callback will help us change the colours of the stars. This logic will be implemented in the setRating
method. The method will need ul
and the event object as parameters to do its job.
Now all we need to do is append the anchor element to the list item element, which can be returned from the function.
This will give as an array of list items, stored in the stars
constant. For performance, we create a document fragment
and append to it each list item element that is in the stars array. We then append the fragment to the ul
element, which in turn gets appended to the element stored in this.starsContainer
.
Implement setRating
Inside the setRating
method, we create the logic that will set the golden colour to the star that is clicked and all the sibling stars that are to its left. But, on every click, we need to reset the colours of all the stars.
setRating(ul, e) {
const listItems = ul.querySelectorAll("li");
const currentId = Number(e.target.id);
for (const item of listItems) {
const a = item.querySelector("a");
a.style.color = "";
if (a.id <= currentId) {
a.style.color = "#ccac00";
} else {
a.style.color = "";
}
}
}
The first thing we are doing inside this function is use the querySelectorAll
method to get all the list item elements that are inside the ul
element. This method returns a Javascript array containing the li
elements. Now we can loop over the elements of the array and get access to the anchor element that is inside each element.
After getting the anchor element of the current item, using querySelector
, we set an empty string to its style’s color
attribute. This is a reset operation.
Then we check if the id
of the current anchor element we are looping over is less than or equal to the id
of the anchor element we just clicked on. We are able to get that id using the target attribute of the event object. If this condition is true, then we set the hex value of the golden colour to the color
attribute of the style of the anchor element. Otherwise, the colour will have an empty string as its value.
Usage
Now, to create the widget, you can go outside of the class and create a new instance using the Stars
constructor.
const stars1 = new Stars(".stars-container1", 5);
You can find the source code here.
Summary
We were able to create a reusable widget with the application of Object Oriented Programming (OOP) principles.
Checkout these TOP JAVASCRIPT courses on Udemy:
1 — The Complete JavaScript Course 2021: From Zero to Expert!
2 — JavaScript: Understanding the Weird Parts
3 — JavaScript — The Complete Guide 2021 (Beginner + Advanced)
4 — Modern Javascript From The Beginning
5 — The Modern JavaScript Bootcamp
Checkout my youtube channel for programming courses and tutorials:
https://www.youtube.com/channel/UCA5ZfHC6koHsukCLzCzxuGA
If you are looking for a Software development job, checkout HIRED.