Undo and Redo Counter
This project is a simple counter that can increase or decrease by some set values, keep track of add
and minus
math operations performed and allow users "undo" and "redo" the operations.
Requirements
- The count should begin at 0
- Clicking the +1, +10, +100 buttons should add 1, 10, and 100 to the count, respectively
- Clicking the -1, -10, -100 buttons should subtract 1, 10, and 100 from the count, respectively
- Clicking any + or - button should show a new entry in the history, in the format: ACTION (BEFORE -> AFTER) (e.g. +1 (0 -> 1))
- Clicking the 'Undo' button should undo the last action. For example, if the user just clicked '+10', clicking undo should subtract 10 from the count
- The user should be able to undo as many operations as possible
- The 'Redo' button should be greyed out until the user clicks 'Undo'
- Clicking the 'Redo' button should redo the last action the user undid. For example, if the user clicked '+10', clicking undo would subtract 10, then clicking redo would add 10 again
- Clicking undo/redo should remove and re-add entries to the history respectively
Problem from FrontendEval1
Link to the React-JS solution will be added below.2
Solution Walkthrough
Defining the base html
First, there are some givens.
- The
undo
andredo
buttons for the operations, - The
increment
anddecrement
values and - The
history
sections.
This section is defined in html as shown below
<section class="body">
<div>
<button id="undo">Undo</button>
<button id="redo">Redo</button>
</div>
<div>
<button class="controlbtn" id="minus100">-100</button>
<button class="controlbtn" id="minus10">-10</button>
<button class="controlbtn" id="minus1">-1</button>
<span id="result">0</span>
<button class="controlbtn" id="plus1">1</button>
<button class="controlbtn" id="plus10">10</button>
<button class="controlbtn" id="plus100">100</button>
</div>
<div>
<h3>History Info</h3>
<div id="history-list"></div>
</div>
</section>
In Javascript script.js
file
Get all the defined elements in the Javascript file
-
result
-> where the resulting value of the operations is shown -
allbtns
-> an array of all the buttons defined in the html file -
historyBox
-> where the operations are listed. Each list item represented asACTION_VALUE (PREVIOUS_RESULT_VALUE -> CURRENT_RESULT_VALUE)
. -
undoBtn
-> on click, returns to the previous state. -
redoBtn
-> on click, redo theundoBtn
action.
const result = document.getElementById("result");
const allbtns = Array.from(document.getElementsByClassName("controlbtn"));
const historyBox = document.getElementById("history-list");
const undoBtn = document.getElementById("undo");
const redoBtn = document.getElementById("redo");
...
Add Listener to all the Buttons
When a user clicks any of the add or minus operation button, fire an event to perform that math operation, display the resulting value and add the item to the history list.
The design choice for ease of use is that each button in the html has been given an id similar to the operation they perform when user clicks them. E.g plus1, minus1, plus10 etc.
Iterate over all the buttons that in allBtns
and attach a click listener event, such that onClick
of each button, it calls a performMathOperation
defined with its value and operator.
...
allbtns.forEach((item, index) => {
switch (item.id) {
case "minus100": {
item.addEventListener("click", (e) => {
performMathOperation(100, "-");
});
return;
}
case "minus10": {
item.addEventListener("click", (e) => {
performMathOperation(10, "-");
});
return;
}
case "minus1": {
item.addEventListener("click", (e) => {
performMathOperation(1, "-");
});
return;
}
case "plus1": {
item.addEventListener("click", (e) => {
performMathOperation(1, "+");
});
return;
}
case "plus10": {
item.addEventListener("click", (e) => {
performMathOperation(10, "+");
});
return;
}
case "plus100": {
item.addEventListener("click", (e) => {
performMathOperation(100, "+");
});
return;
}
}
});
...
The performMathOperation
accepts the value
and operator
as arguments. It does 4 things:
- If the
operator
is+
, it adds it to the displayed result value and if theoperator
is-
, it subtracts it from the displayed result value. - It updates the
historyList
with an object that contains the current operator (ops
), action value (actionVal
), previous result value (prevVal
) and current result value (afterVal
). - It calls a
createHistoryItem
that creates and append the current item in the expected format to the list displayed on the screen. - Finally, it call
updateButtons
function that checks whether to disable or enable theundo
andredo
button.
...
function performMathOperation(val, ops) {
const actionVal = val;
const prevVal = displayedRes;
if (ops === "+") {
displayedRes += val;
} else if (ops === "-") {
displayedRes -= val;
}
result.textContent = displayedRes;
const afterVal = displayedRes;
const res = ` ${ops}${actionVal} \t (${prevVal} -> ${afterVal})`;
historyList.push({ ops, actionVal, prevVal, afterVal });
historyBox.appendChild(createHistoryItem(res));
updateButtons();
}
function createHistoryItem(item) {
const p = document.createElement("p");
p.textContent = item;
return p;
}
function updateButtons() {
undoBtn.disabled = historyList.length === 0 ? true : false;
redoBtn.disabled = redoHistoryList.length === 0 ? true : false;
}
...
Handling Undo Action
When a user clicks the undoBtn
, reverse the operation performed using the history item stored in our historyList
array.
First, if the historyList is empty, stop operation immediately. This is a redundant check because the button will be disabled if it is but also a good idea to double make sure.
Then, get the most recent object pushed to the history list by popping and use the saved properties to calculate our previous values. To get the previous state of the values, do the inverse of the saved operator.
If the current saved previous operator is plus, minus the actionVal
(value on clicked button) from the afterVal
(result after the math operation) to get the newAfterVal
old result after the math operation. Then update the displayedRes
(result on screen)
...
const { ops, prevVal, afterVal, actionVal } = historyList.pop();
let newAfterVal;
if (ops === "+") {
newAfterVal = afterVal - actionVal;
displayedRes -= actionVal;
} else if (ops === "-") {
newAfterVal = afterVal + actionVal;
displayedRes += actionVal;
}
result.textContent = displayedRes;
...
First steps to Redo
To Redo what the user has just Undo, keep track of the newly computed values in a separate list called redoHistoryList
.
To update the redoHistoryList
with the current info, update the afterVal
with the new current resulting value (newAfterVal
) and the current previous value is afterVal
.
Then, remove the last element displayed in historyList from the screen
The full code when undoBtn
is clicked:
...
undoBtn.addEventListener("click", (e) => {
if (historyList.length === 0) {
return;
}
const { ops, prevVal, afterVal, actionVal } = historyList.pop();
let newAfterVal;
if (ops === "+") {
newAfterVal = afterVal - actionVal;
displayedRes -= actionVal;
} else if (ops === "-") {
newAfterVal = afterVal + actionVal;
displayedRes += actionVal;
}
result.textContent = displayedRes;
redoHistoryList.push({
ops,
afterVal: newAfterVal,
actionVal,
prevVal: afterVal,
});
if (historyBox.lastChild) {
historyBox.removeChild(historyBox.lastChild);
}
updateButtons();
});
...
Handling Redo Action
Redoing an action is simply just recomputing the math operation like a user clicks a button. Hence, get the most recent object in the redoHistoryList and call the performMathOperation
with the saved actionVal
(the value on the button clicked by the user) and its operator as parameters.
...
redoBtn.addEventListener("click", (e) => {
if (redoHistoryList.length === 0) {
return;
}
const { ops, actionVal } = redoHistoryList.pop();
performMathOperation(actionVal, ops);
});
...
Nice to Have - Implement Ctrl+Z (undo) and Ctrl+Shift+Z (redo)
Finally, add a keydown event listener to the global document that triggers the click event or action on the undo and redo buttons when the user uses the keyboard combinations Ctrl+Z (undo) and Ctrl+Shift+Z (redo).
...
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
e.preventDefault();
undoBtn.click();
}
if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
e.preventDefault();
redoBtn.click();
}
});
...
Final Result
Link to the Github repository
-
Walkthrough of the React javascript solution ↩
Top comments (0)