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 undoandredobuttons for the operations,
- The incrementanddecrementvalues and
- The historysections.
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 theundoBtnaction.
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 operatoris+, it adds it to the displayed result value and if theoperatoris-, it subtracts it from the displayed result value.
- It updates the historyListwith an object that contains the current operator (ops), action value (actionVal), previous result value (prevVal) and current result value (afterVal).
- It calls a createHistoryItemthat creates and append the current item in the expected format to the list displayed on the screen.
- Finally, it call updateButtonsfunction that checks whether to disable or enable theundoandredobutton.
...
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)