DEV Community

OlumideSamuel
OlumideSamuel

Posted on

Undo and Redo Counter - A must know Vanilla Javascript Frontend Interview Question (Part 3)

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

Sketch of the Problem

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 and redo buttons for the operations,
  • The increment and decrement 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>
Enter fullscreen mode Exit fullscreen mode

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 as ACTION_VALUE (PREVIOUS_RESULT_VALUE -> CURRENT_RESULT_VALUE).
  • undoBtn -> on click, returns to the previous state.
  • redoBtn -> on click, redo the undoBtn 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");
...
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
});
...
Enter fullscreen mode Exit fullscreen mode

The performMathOperation accepts the value and operator as arguments. It does 4 things:

  1. If the operator is +, it adds it to the displayed result value and if the operator is -, it subtracts it from the displayed result value.
  2. 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).
  3. It calls a createHistoryItem that creates and append the current item in the expected format to the list displayed on the screen.
  4. Finally, it call updateButtons function that checks whether to disable or enable the undo and redo 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;
}

...
Enter fullscreen mode Exit fullscreen mode

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;
...
Enter fullscreen mode Exit fullscreen mode

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();
});
...
Enter fullscreen mode Exit fullscreen mode

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);
});
...
Enter fullscreen mode Exit fullscreen mode

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();
  }
});
...
Enter fullscreen mode Exit fullscreen mode

Final Result

the final result

Link to the Github repository


  1. https://frontendeval.com/questions/undoable-counter 

  2. Walkthrough of the React javascript solution 

Top comments (0)