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 Vanilla-JS solution will be added below.2
Solution Walkthrough
Defining the base states
- The project requires to add or subtract a value from 0 (
result
) as the user clicks on the defined buttons. - Then, list each of this operation in the format
ACTION_VALUE (PREVIOUS_RESULT_VALUE -> CURRENT_RESULT_VALUE)
.
Note:
-
actionVal
: represents the value of the clicked button. represents by what amount to increase or decrease the result -
prevVal
: represents the previous result before the new math operation is done -
afterVal
: represents the current result of the math operation- When the
Undo
button is clicked, reverse the last operation - When the
Redo
button is clicked, undo the lastredo
action
- When the
Hence, define the local states in our App.js
as:
...
export type HistoryType = {
actionVal: number;
prevVal: number;
afterVal: number;
};
function App() {
const [historyList, setHistoryList] = useState<HistoryType[]>([]);
const [result, setResult] = useState(0);
const [redoHistoryList, setRedoHistoryList] = useState<HistoryType[]>([]);
...
}
Defining the base components
Doable Controls
This component controls the Undo and Redo click actions on the buttons.
...
type DoableControlsProp = {
handleUndo: () => void;
handleRedo: () => void;
historyList: HistoryType[];
redoHistoryList: HistoryType[];
};
export const DoableControls: FC<DoableControlsProp> = ({
handleUndo,
handleRedo,
historyList,
redoHistoryList,
}) => {
return (
<div>
<button
className="controlbtn"
id={"undoBtn"}
onClick={handleUndo}
disabled={historyList.length === 0}
>
Undo
</button>
<button
className="controlbtn"
id="redoBtn"
onClick={handleRedo}
disabled={redoHistoryList.length === 0}
>
Redo
</button>
</div>
);
};
Main Controls
The results, increment and decrement buttons are defined in the component. Since the defined actions are finite, let's set each button to have an id
that defines the action it triggers for ease of use. E.g plus1, minus1, plus10 etc.
When a user clicks a button, pass the value defined on the button to the parent container, and update the list of objects in the history with the current result (prevVal
), the just clicked button value (btnValue
), and the new result after operation (afterVal
) is done.
const MainControls: FC<MainControlsProps> = ({
handleHistoryUpdate,
result,
setResult,
}) => {
const handleOnBtnPress = (event: React.MouseEvent<HTMLButtonElement>) => {
const target = event.target as HTMLButtonElement;
const btnValue = parseInt(target.innerText);
const newResult = result + btnValue;
setResult(btnValue);
handleHistoryUpdate({
actionVal: btnValue,
prevVal: result,
afterVal: newResult,
});
};
return (
<div>
<button className="controlbtn" id="minus100" onClick={handleOnBtnPress}>
-100
</button>
<button className="controlbtn" id="minus10" onClick={handleOnBtnPress}>
-10
</button>
<button className="controlbtn" id="minus1" onClick={handleOnBtnPress}>
-1
</button>
<span id="result">{result}</span>
<button className="controlbtn" id="plus1" onClick={handleOnBtnPress}>
1
</button>
<button className="controlbtn" id="plus10" onClick={handleOnBtnPress}>
10
</button>
<button className="controlbtn" id="plus100" onClick={handleOnBtnPress}>
100
</button>
</div>
);
};
export default MainControls;
Rendering History of Past Operations
To render the history of the past operations, map through the historyList
state and format the data as
ACTION_VALUE (PREVIOUS_RESULT_VALUE -> CURRENT_RESULT_VALUE)
.
// in App.js
...
<div>
<h3>History Info</h3>
{historyList.map((item, index) => {
return (
<div key={index}>
{item.actionVal} ({item.prevVal} {`->`} {item.afterVal})
</div>
);
})}
</div>
...
On History Update
When a user clicks any of the MainControls buttons to add or substract from the result, define a function to update the history and results.
Updating result will always work with the plus
operation because from elementary maths,
2 + 1 = 3
and 2 + -1 => 2 - 1 = 1
Also, because we want the most recent operation to be added to the top of the history list, add the item before appending the rest behind it. That is, always at index 0.
...
const onUpdateHistory = (payload: HistoryType) => {
// adding the most recent to top of list
setHistoryList((prev) => [payload, ...prev]);
};
const handleUpdateResult = (btnValue: number) => {
setResult((prev: number) => prev + btnValue);
};
...
Handling Undo Operation
Remember this format from above
ACTION_VALUE (PREVIOUS_RESULT_VALUE -> CURRENT_RESULT_VALUE)
where
- ACTION_VALUE = value on button clicked
- PREVIOUS_RESULT_VALUE = the default result before the math operation
- CURRENT_RESULT_VALUE = the new result after the math operation
Then onClick the undoBtn
, abort this operation if historyList
is empty.
In the vanilla-js solution to this project, new items are added to the bottom of the list. However, in this solution, new items are added to the top of the list.
Hence, updating the list requires getting the just added element in index 0 using the javascript shift
inorder to recalculate the previous state.
To get the previous result (newAfterVal
), substract the previous actionVal
(the value on previously clicked button) from the old result (afterVal
).
This implies => newAfterVal = afterVal - actionVal;
Then update the result
, historyList
and redoHistoryList
with the new data.
...
const handleUndo = () => {
if (historyList.length === 0) {
return;
}
const tempList = [...historyList];
const shifted = tempList.shift();
if (!shifted) {
return;
}
const { actionVal, afterVal, prevVal } = shifted;
let newAfterVal = result;
newAfterVal = afterVal - actionVal;
setResult(newAfterVal);
setHistoryList(tempList);
setRedoHistoryList((prev) => [
...prev,
{ afterVal: newAfterVal, prevVal, actionVal },
]);
};
...
Handling Redo Operation
The redo operation gets the most recent undo
object added to the top of the redoHistoryList
by the undo action, and calculates the new result.
...
const handleRedo = () => {
if (redoHistoryList.length === 0) {
return;
}
const popped = redoHistoryList.pop();
if (!popped) {
return;
}
const { actionVal, prevVal } = popped;
const newRes = result + actionVal;
setResult((prev) => prev + actionVal);
onUpdateHistory({ actionVal, prevVal, afterVal: newRes });
};
...
The End Result
Link to the Github repository
-
Walkthrough of the vanilla javascript solution ↩
Top comments (0)