Reputation: 27
My goal is to have a button which runs Javascript roshtimer()
and also have hotkeys available to run the command. For instance, I'd like my user to have the option of clicking the button on their screen OR pressing 'r' on their keyboard.
I've had quite a bit of trouble with this issue, as I'm relatively new to React.
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
time: 0,
roshDead: false,
roshDeathTime: 0,
aegisExpireTime: 0,
roshRespawnTime1: 0,
roshRespawnTime2: 0,
start: 0,
isOn: false,
};
this.startTimer = this.startTimer.bind(this);
this.stopTimer = this.stopTimer.bind(this);
this.resetTimer = this.resetTimer.bind(this);
this.roshTimer = this.roshTimer.bind(this);
this.roshButton = React.createRef();
this.handleRoshHotkey = this.handleRoshHotkey.bind(this);
}
componentDidMount() {
document.addEventListener("keydown", this.handleRoshHotkey);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.handleRoshHotkey);
}
handleRoshClick() {
console.log("button was clicked!");
this.roshTimer();
}
handleRoshHotkey = (e) => {
if (e.key === "r" || e.keyCode === 13) {
console.log("hotkey was pressed!");
this.roshTimer();
}
};
startTimer() {
...
}
stopTimer() {
...
}
resetTimer() {
...
}
roshTimer() {
//compute rosh timers
let roshDeathTime = this.state.time;
let newAegisExpire = this.state.time + 300000; //5 mins = 300000 milliseconds
let newRespTime1 = this.state.time + 480000; // 8 mins = 480000 milliseconds
let newRespTime2 = this.state.time + 660000; // 11 mins = 660000 milliseconds
this.setState({
roshDeathTime: this.state.time,
aegisExpireTime: newAegisExpire,
roshRespawnTime1: newRespTime1,
roshRespawnTime2: newRespTime2,
roshDead: true,
});
//format rosh timers
...
//log to console for my own sanity
console.log(roshDeathTime, newAegisExpire, newRespTime1, newRespTime2);
//copy to user's clipboard
navigator.clipboard.writeText(
newAegisExpire + " " + newRespTime1 + " " + newRespTime2
);
}
render() {
let start =
this.state.time == 0 ? (
<button onClick={this.startTimer}>start</button>
) : null;
let stop = this.state.isOn ? (
<button onClick={this.stopTimer}>stop</button>
) : null;
let reset =
this.state.time != 0 && !this.state.isOn ? (
<button class="red" onClick={this.resetTimer}>
reset
</button>
) : null;
let resume =
this.state.time != 0 && !this.state.isOn ? (
<button class="green" onClick={this.startTimer}>
resume
</button>
) : null;
let rosh = this.state.isOn ? (
<div className={this.state.focused ? "focused" : ""}>
<button
onClick={() => this.handleRoshClick()}
autoFocus
ref={(c) => (this._input = c)}
>
{" "}
Rosh died!{" "}
</button>
</div>
) : null;
return (
<div>
<h3>
Timer: {prettyMilliseconds(this.state.time, { colonNotation: true })}
</h3>
{start}
{resume}
{stop}
{reset}
<div ref={this.roshButton} autofocus onKeyDown={() => this.handleRoshHotkey()} tabIndex={1}>
{rosh}
</div>
</div>
);
}
}
export default Timer;
At the moment, I can run my app and spam the "Rosh died!" button and see the expected output. If I click anywhere on the webpage, I can then use the hotkeys to get the expected output. However, if I use the hotkeys BEFORE manually clicking the button, the app crashes and I'm greeted with
TypeError: Cannot read property 'key' of undefined
further stating that my issue is here:
handleRoshHotkey = (e) => {
if (e.key === "r" || e.keyCode === 13){ <-------------
console.log("hotkey was pressed!");
this.roshTimer();
}
};
Here's a sample console log up until it crashes:
button was clicked!
1134 "5:01" "8:01" "11:01"
button was clicked!
1878 "5:01" "8:01" "11:01"
hotkey as pressed!
4318 "5:04" "8:04" "11:04"
hotkey was pressed!
5338 "5:05" "8:05" "11:05"
button was clicked!
8142 "5:08" "8:08" "11:08"
Uncaught TypeError: Cannot read property 'key' of undefined
.
. //(hundreds of lines of traceback follow)
.
hotkey was pressed!
11194 "5:11" "8:11" "11:11"
Thanks!
Upvotes: 0
Views: 1080
Reputation: 1075129
Two things:
The main problem is when you're rendering, you're doing
onKeyDown={() => this.handleRoshHotkey()}
...which calls handleRoshHotkey
with no arguments, which is why you get the error. Since you've used bind
in the constructor, change that to just:
onKeyDown={this.handleRoshHotkey}
Using the handler in React is sufficient, you don't also want to use it via addEventListener
, so remove that code (I'm guessing you added it for debugging). You can just completely remove componentDidMount
and componentWillUnmount
.
Upvotes: 1
Reputation: 8446
This should probably just be a comment, but you're not passing the event in onKeyDown={() => this.handleRoshHotkey()}
.
Since the function is already bound, just pass onKeyDown={this.handleRoshHotkey}
and the event will be piped.
Upvotes: 0
Reputation: 706
You forgot to pass the event:
<div ref={this.roshButton} autofocus onKeyDown={(e) => this.handleRoshHotkey(e)} tabIndex={1}>
{rosh}
</div>
but better:
<div ref={this.roshButton} autofocus onKeyDown={this.handleRoshHotkey} tabIndex={1}>
{rosh}
</div>
Upvotes: 0