React Keyup event on document within react lifesycle - javascript

It is my understanding that refs are not defined outside the react lifecycle (source). The problem I am trying to solve is to capture a key press at the document level (i.e. trigger the event no matter what element is in focus), and then interact with a react ref. Below is a simplified example of what I am trying to do:
export default class Console extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
text: "",
};
}
print(output: string) {
this.setState({
text: this.state.text + output + "\n"
})
}
toggleVisible()
{
this.setState({visible: !this.state.visible});
}
render() {
const footer_style = {
display: this.state.visible ? "inline" : "none",
};
return (
<footer id="console-footer" className="footer container-fluid fixed-bottom" style={footer_style}>
<div className="row">
<textarea id="console" className="form-control" rows={5} readOnly={true}>{this.state.text}</textarea>
</div>
</footer>
);
}
}
class App extends React.Component {
private console: Console;
constructor() {
super({});
this.console = React.createRef();
}
keyDown = (e) =>
{
this.console.current.toggleVisible(); // <-- this is undefined
}
componentDidMount(){
document.addEventListener("keydown", this.keyDown);
},
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDown);
},
render() {
return (
<div className="App" onKeyDown={this.keyDown}> // <-- this only works when this element is in focus
// other that has access to this.console that will call console.print(...)
<Console ref={this.console} />
</div>
);
}
}
My question is: is there a way to have this sort of document level key press within the lifesycle of react so that the ref is not undefined inside the event handler keyDown? I've seen a lot of solutions that involve setting the tabIndex and hacking to make sure the proper element has focus at the right time, but these do not seem like robust solutions to me.
I'm just learning React so maybe this is a design limitation of React or I am not designing my components properly. But this sort of functionality seems quite basice to me, having the ability to pass components from one to the other and call methods on eachother.

You're calling the onKeyDown callback twice, once on document and once on App.
Events bubble up the tree.
When the textarea is not in focus, only document.onkeydown is called.
When it is in focus, both document.onkeydown and App's div.onkeydown are called, effectively cancelling the effect(toggling state off and back on).
Here's a working example: https://codesandbox.io/s/icy-hooks-8zuy7?file=/src/App.js
import React from "react";
class Console extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
text: ""
};
}
print(output: string) {
this.setState({
text: this.state.text + output + "\n"
});
}
toggleVisible() {
this.setState({ visible: !this.state.visible });
}
render() {
const footer_style = {
display: this.state.visible ? "inline" : "none"
};
return (
<footer
id="console-footer"
className="footer container-fluid fixed-bottom"
style={footer_style}
>
<div className="row">
<textarea id="console" className="form-control" rows={5} readOnly>
{this.state.text}
</textarea>
</div>
</footer>
);
}
}
export default class App extends React.Component {
constructor(props) {
super(props);
this.console = React.createRef();
}
keyDown = (e) => {
this.console.current.toggleVisible(); // <-- this is undefined
};
componentDidMount() {
document.addEventListener("keydown", this.keyDown);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDown);
}
render() {
return (
<div className="App" style={{ backgroundColor: "blueviolet" }}>
enter key to toggle console
<Console ref={this.console} />
</div>
);
}
}
Also, I recommend using react's hooks:
export default App = () => {
const console = React.createRef();
const keyDown = (e) => {
console.current.toggleVisible(); // <-- this is undefined
};
React.useEffect(() => {
// bind onComponentDidMount
document.addEventListener("keydown", keyDown);
// unbind onComponentDidUnmount
return () => document.removeEventListener("keydown", keyDown);
});
return (
<div className="App" style={{ backgroundColor: "blueviolet" }}>
press key to toggle console
<Console ref={console} />
</div>
);
};

Related

Stop Relay: Query Renderer in reloading data for certain setStates

I'm currently following this and I did get it to work. But I would like to know if there is a way to stop the Query Render from reloading the data when calling this.setState(). Basically what I want is when I type into the textbox, I don't want to reload the data just yet but due to rendering issues, I need to set the state. I want the data to be reloaded ONLY when a button is clicked but the data will be based on the textbox value.
What I tried is separating the textbox value state from the actual variable passed to graphql, but it seems that regardless of variable change the Query will reload.
Here is the code FYR.
const query = graphql`
query TestComponentQuery($accountId: Int) {
viewer {
userWithAccount(accountId: $accountId) {
name
}
}
}
`;
class TestComponent extends React.Component{
constructor(props){
super(props);
this.state = {
accountId:14,
textboxValue: 14
}
}
onChange (event){
this.setState({textboxValue:event.target.value})
}
render () {
return (
<div>
<input type="text" onChange={this.onChange.bind(this)}/>
<QueryRenderer
environment={environment}
query={query}
variables={{
accountId: this.state.accountId,
}}
render={({ error, props }) => {
if (error) {
return (
<center>Error</center>
);
} else if (props) {
const { userWithAccount } = props.viewer;
console.log(userWithAccount)
return (
<ul>
{
userWithAccount.map(({name}) => (<li>{name}</li>))
}
</ul>
);
}
return (
<div>Loading</div>
);
}}
/>
</div>
);
}
}
Okay so my last answer didn't work as intended, so I thought I would create an entirely new example to demonstrate what I am talking about. Simply, the goal here is to have a child component within a parent component that only re-renders when it receives NEW props. Note, I have made use of the component lifecycle method shouldComponentUpdate() to prevent the Child component from re-rendering unless there is a change to the prop. Hope this helps with your problem.
class Child extends React.Component {
shouldComponentUpdate(nextProps) {
if (nextProps.id === this.props.id) {
return false
} else {
return true
}
}
componentDidUpdate() {
console.log("Child component updated")
}
render() {
return (
<div>
{`Current child ID prop: ${this.props.id}`}
</div>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
id: 14,
text: 15
}
}
onChange = (event) => {
this.setState({ text: event.target.value })
}
onClick = () => {
this.setState({ id: this.state.text })
}
render() {
return (
<div>
<input type='text' onChange={this.onChange} />
<button onClick={this.onClick}>Change ID</button>
<Child id={this.state.id} />
</div>
)
}
}
function App() {
return (
<div className="App">
<Parent />
</div>
);
}

Why can't update a state when use onKeyPress [duplicate]

This question already has answers here:
How to handle the `onKeyPress` event in ReactJS?
(12 answers)
Closed 4 years ago.
Please considering this follow code, I can't update inputVal when I using a Keypress event handler.
import React, { Component, Fragment } from 'react';
import List from './List';
import './ListTodos.css';
class Todos extends Component {
constructor(props) {
super(props);
this.state = {
inputVal: null
}
this.refInput = null
this._handleChange = this._handleChange.bind(this)
}
_handleChange(pEvt) {
if (pEvt.keyCode === "13") {
this.setState({
inputVal: this.refInput.value
})
console.log(this.state.refInput)
}
}
render() {
const { text } = this.props;
return (
<Fragment>
<div className="form">
<input ref={input => {this.refInput = input}} onKeyDown={pEvt => this._handleChange(pEvt)} className="form__input" placeholder={ text } />
<div>
<List TxtVal={this.state.inputVal} />
</div>
</div>
</Fragment>
)
}
}
export default Todos;
I really dont like using on onKeyDown. Instead you can use onChange which i think its better.
So Basically you need can do this too.
class Todos extends Component {
constructor(props) {
super(props);
this.state = {
inputVal: null
}
this._handleChange = this._handleChange.bind(this)
}
_handleChange(e) {
if (e.keyCode === "13") {
this.setState({
inputVal: e.target.value
})
console.log(e.target.value)
}
}
render() {
const { text } = this.props;
return (
<Fragment>
<div className="form">
<input name="todo" onChange={(e) => this._handleChange(e)} className="form__input" placeholder={ text } />
<div>
<List TxtVal={this.state.inputVal} />
</div>
</div>
</Fragment>
)
}
}
export default Todos;
Use pEvt.target.value instead of this.refInput.value
_handleChange(pEvt) {
if (pEvt.keyCode === "13") {
this.setState({
inputVal: pEvt.target.value
});
console.log(this.state.inputVal);
}
}
You're actually using the KeyDown event in your code instead of KeyPress as you asserted. It looks like you're just trying to get the value of the input element right?
I'd create a handler for the onchange event instead for the input. You're just trying to get the value of the input. And you wouldn't even need your ref.
_handleChange(e) {
this.setState({
inputVal: e.target.value
});
}
constructor() {
// wire up your event handler
this._handleChange = this._handleChange.bind(this);
}
...
<input onChange={this._handleChange} className="form__input" placeholder={ text } />

ReactJS - Stop button onClick from taking focus from <input>

So I've set up a file to setState() for onBlur and onFocus in the SocialPost.js file. But when I onClick a <div> in the SocialPostList.js (the parent) where it activates the parameterClicked() function in the SocialPost.js file, the <input> in SocialPost.js becomes blurred.
How do I make it so that the <button> onClick in SocialPostList.js does not take the focus() from the <input> in SocialPost.js?
I've tried e.preventDefault() and e.stopPropagation() without success. The files are below, any help would be appreciated!!!
SocialPostList.js
import React, { Component } from 'react'
import { graphql, gql } from 'react-apollo'
import SocialPost from './SocialPost'
class SocialPostList extends Component {
render() {
const PostListArray = () => {
return(
<div onClick={(e) => {e.preventDefault(); e.stopPropagation()}}>
{this.props.allParametersQuery.allParameters.map((parameter, index) => (
<div
key={index}
onClick={(e) => {e.preventDefault();e.stopPropagation();this.child.parameterClicked(parameter.param, parameter.id)}}
>{'{{' + parameter.param + '}}'}</div>
))}
</div>)
}
return (
<div>
...
<PostListArray />
{this.props.allSocialPostsQuery.allSocialPosts.map((socialPost, index) => (
<SocialPost
ref={instance => {this.child = instance}}
key={socialPost.id}
socialPost={socialPost}
index={index}
deleteSocialPost={this._handleDeleteSocialPost}
updateSocialPost={this._handleUpdateSocialPost}
allParametersQuery={this.props.allParametersQuery}/>
))}
...
</div>
)
}
}
const ALL_SOCIAL_POSTS_QUERY = gql`
query AllSocialPostsQuery {
allSocialPosts {
id
default
message
}}`
export default graphql(ALL_SOCIAL_POSTS_QUERY, {name: 'allSocialPostsQuery'})(SocialPostList)
SocialPost.js
import React, { Component } from 'react'
class SocialPost extends Component {
constructor(props) {
super(props)
this.state = {
message: this.props.socialPost.message,
focus: false
}
this._onBlur = this._onBlur.bind(this)
this._onFocus = this._onFocus.bind(this)
}
_onBlur() {
setTimeout(() => {
if (this.state.focus) {
this.setState({ focus: false });
}}, 0);
}
_onFocus() {
if (!this.state.focus) {
this.setState({ focus: true });
}
}
render() {
return (
<div className='socialpostbox mb1'>
<div className='flex'>
<input
onFocus={this._onFocus}
onBlur={this._onBlur}
type='text'
value={this.state.message}
onChange={(e) => { this.setState({ message: e.target.value})}}/>
</div>
</div>
)
}
parameterClicked = (parameterParam) =>{
if (!this.state.focus) return
let message = this.state.message
let newMessage = message.concat(' ' + parameterParam)
this.setState({ message: newMessage })
}
export default SocialPost
Well, I don't think that's a React thing. It appears the blur event fires before the onClick, so the latter cannot prevent the former, and I'd expect event.stopPropagation to stop events bubbling from child to parent, not the other way around. In other words, I don't know how to stop it.
In all fairness this behaviour is expected - clicking somewhere else makes you lose focus. That said, here and elsewhere a solution is presented where you set up a flag on mouse down. Then, when blur fires, if it encounters the 'click flag' it may abstain from producing effects and may even refocus back.
If you choose to refocus, it is trivial to save a reference to the button or input, or querySelecting it (it's not too late or anything like that). Just be cautious that it is all too easy to set focus traps or mess up navigation for screen readers when you mix focus and javascript.

How do you stop propagation in ReactJS [duplicate]

This question already has answers here:
Prevent onClick event by clicking on a child div
(2 answers)
How to call stopPropagation in reactjs?
(2 answers)
Closed 5 years ago.
Can someone help explain how to stop event propagation on a click? I've read many other posts, but I still can't figure it out.
I have a grid of 40x50 boxes, and when I click one I'd like to see that box's id. Currently, when I click it bubbles up and returns Board as what's been clicked. So I need to stop the propagation, right? Where/how do I do that? I've tried passing i.stopPropagation(); in the handleClick() method, but it tells me that i.stopPropagation(); isn't a function.
function Square(props) {
return (
<div className="square" id={props.id} onClick={props.onClick}/>
);
}
class Board extends Component {
rowsByColumns(rows, columns) {
let arr=[];
let k=0;
let m=0
for (let i=0;i<rows;i++) {
arr.push([])
for (let j=0;j<columns;j++) {
arr[i].push(<Square key={"square"+m++} id={"square"+m} living={false} onClick={() => this.props.onClick(this)}/>)
}
}
let newArr = arr.map(row => <Row key={"row"+k++}>{row}</Row>);
return (newArr);
}
render() {
return (
<div id="board">
{this.rowsByColumns(40,50)}
</div>
);
}
}
class App extends Component {
constructor() {
super();
this.state = {
alive: ["square1"],
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(i) {
console.log(i);
this.setState({
alive: this.state.alive.concat([i])
})
}
render() {
return (
<div className="App container-fluid">
<main className="row justify-content-center">
<Board alive={this.state.alive} onClick={i => this.handleClick()}/>
</div>
</main>
</div>
);
}
}
Your click handler receives an event object. Use stopPropagation on it:
handleClick(e) {
e.stopPropagation();
}
then in your onClick:
onClick={this.handleClick}
Live example — the child stops every other click it sees:
class Parent extends React.Component {
constructor(...args) {
super(...args);
this.handleClick = () => {
console.log("Parent got the click");
};
}
render() {
return <div onClick={this.handleClick}>
Click here to see parent handle it.
<Child />
</div>;
}
}
class Child extends React.Component {
constructor(...args) {
super(...args);
this.stopClick = true;
this.handleClick = e => {
if (this.stopClick) {
e.stopPropagation();
console.log("Child got the click and stopped it");
} else {
console.log("Child got the click and didn't stop it");
}
this.stopClick = !this.stopClick;
};
}
render() {
return <div onClick={this.handleClick}>I'm the child</div>;
}
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);
<div id="root"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

change scrollTop in reactjs

I just learn react, and want to achieve a function :
both A,B are components, if A scroll, then B scroll
The following is my code
<A onScroll="handleScroll"></A>
//what i write now
handleScroll: function(event){
var target = event.nativeEvent.target;
//do something to change scrollTop value
target.scrollTop += 1;
// it looks the same as not use react
document.getElementById(B).scrollTop = target.scrollTop;
}
but actually I want my code like this
//what i want
<A scrollTop={this.props.scrollSeed}></A>
<B scrollTop={this.props.scrollSeed}></B>
//...
handleScroll(){
this.setState({scrollSeed: ++this.state.scrollSeed})
}
it is similar to input
<input value="this.props.value"/>
<input value="this.props.value"/>
<input ref='c' onChange={handleChange}>
//...
handleChange: function() {
// enter some code in c and then render in a and b automatically
}
In other words, I want some attribute, like scrollTop(different
form <input value={}> ,because <A scrollTop={}> doesn't work) ,is bind with some state, so that I can just use setState, and they will update by themselves.
I googled before but can't find the answser. I hope that my poor English won't confuse you.
There are a number of patterns to achieve this. This sample is what I came up with to get you up and going.
First create a component class which has an oversize element for scroll effect. When dragging the scroll bar, this component calls its handleScroll React property to notify its parent component, with the value of scrollTop.
var Elem = React.createClass({
render() {
return (
<div ref="elem"
onScroll={ this.onScroll }
style={{ width: "100px", height: "100px", overflow: "scroll" }}>
<div style={{ width: "100%", height: "200%" }}>Hello!</div>
</div>
);
},
componentDidUpdate() {
this.refs.elem.scrollTop = this.props.scrollTop;
},
onScroll() {
this.props.handleScroll( this.refs.elem.scrollTop );
}
});
The parent component, aka wrapper, keeps the scroll top value in its state. Its handleScroll is passed to the child components as callback. Any scroll on the child elements triggers the callback, sets the state, results in a redraw, and updates the child component.
var Wrapper = React.createClass({
getInitialState() {
return {
scrollTop: 0
}
},
render() {
return (
<div>
<Elem scrollTop={ this.state.scrollTop } handleScroll={ this.handleScroll } />
<Elem scrollTop={ this.state.scrollTop } handleScroll={ this.handleScroll } />
</div>
);
},
handleScroll( scrollTop ) {
this.setState({ scrollTop });
}
});
And render the wrapper, presuming an existing <div id="container"></div>.
ReactDOM.render(
<Wrapper />,
document.getElementById('container')
);
2019's answer
First, the fix:
const resetScrollEffect = ({ element }) => {
element.current.getScrollableNode().children[0].scrollTop = 0
}
const Table = props => {
const tableRef = useRef(null)
useEffect(() => resetScrollEffect({ element: tableRef }), [])
return (
<Component>
<FlatList
ref={someRef}
/>
</Component>
)
}
Second, a little explanation:
Idk what is your reason why you got here but I have used flex-direction: column-reverse for my FlatList (it's a list of elements). And I need this property for z-index purposes. However, browsers set their scroll position to the end for such elements (tables, chats, etc.) - this may be useful but I don't need that in my case.
Also, example is shown using React Hooks, but you can use older more traditional way of defining refs
this.refs is deprecated. use reactjs.org/docs/refs-and-the-dom.html#creating-refs
import React from 'react';
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.resultsDiv = React.createRef();
}
someFunction(){
this.resultsDiv.current.scrollIntoView({behavior: 'smooth'});
// alternative:
// this.resultsDiv.current.scrollTop = 0;
}
render() {
return (
<div ref={this.resultsDiv} />
);
}
}
export default SomeComponent;
Here's an updated version of Season's answer, including a runnable snippet. It uses the recommended method for creating refs.
class Editor extends React.Component {
constructor(props) {
super(props);
this.content = React.createRef();
this.handleScroll = this.handleScroll.bind(this);
}
componentDidUpdate() {
this.content.current.scrollTop = this.props.scrollTop;
}
handleScroll() {
this.props.onScroll( this.content.current.scrollTop );
}
render() {
let text = 'a\n\nb\n\nc\n\nd\n\ne\n\nf\n\ng';
return <textarea
ref={this.content}
value={text}
rows="10"
cols="30"
onScroll={this.handleScroll}/>;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {scrollTop: 0};
this.handleScroll = this.handleScroll.bind(this);
}
handleScroll(scrollTop) {
this.setState({scrollTop: scrollTop});
}
render() {
return <table><tbody>
<tr><th>Foo</th><th>Bar</th></tr>
<tr>
<td><Editor
scrollTop={this.state.scrollTop}
onScroll={this.handleScroll}/></td>
<td><Editor
scrollTop={this.state.scrollTop}
onScroll={this.handleScroll}/></td>
</tr>
</tbody></table>;
}
}
ReactDOM.render(
<App/>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Categories