I have the below react component which is essentially a chat-box
render(){
const messages = this.props.messages;
return(
<div id="project_chat">
<h1>{this.props.project[0].project}</h1>
<div className="chat_room">
<div className="messages" ref="messages">
<Waypoint onEnter={this.activateWayPoint}/>
<ul>
{messages.map((message) => {
return(
<Message key={uuid.v4()} message={message}/>
)
})}
</ul>
</div>
<div className="chat_message_box">
<input type='text' onChange={this.handleChange} value={this.state.message} className="message_box" placeholder="enter message"/>
<button className="submit_message" onClick={this.handleSubmit}>Submit</button>
</div>
</div>
</div>
)
}
the problem i faced is the chat messages box starts at the topmost position of the container (scroll position starts at the top). I wanted the scroll position to be at the bottom like a normal chat room.
so i tried doing this:
componentDidMount(){
this.refs.messages.scrollTop = this.refs.messages.scrollHeight
}
this triggers AFTER the component gets mounted i.e - the message box scroll position initially starts at the top and forces its way to the bottom on render.
this is normally fine but i'm using a library called react-waypoint which would help me paginate chat messages. this gets triggered every time i'm at the top of the container.
the unhappy consequence is that because the message box starts at the top initially on mount, the waypoint always gets triggered on mount as well.
my question is whether i can force the message component to start at the bottom position as opposed to starting the top and going to the bottom at the beginning
I tried doing this
componentWillMount(){
this.refs.messages.scrollTop = this.refs.messages.scrollHeight
}
the problem is i dont have access to refs before the component mounts. is there any other way?
What you want is to avoid firing this.activateWayPoint before you've set scrollTop.
You can do this by setting a state variable waypointReady to false initially. Set it to true in componentDidMount.
Then, you can modify this.activateWayPoint to check this.state.waypointReady, and return immediately if it is false.
// inside component
getInitialState() {
return { waypointReady : false }
}
componentDidMount() {
this.refs.messages.scrollTop = this.refs.messages.scrollHeight;
this.setState({ waypointReady : true});
}
activateWayPoint() {
if (! this.state.waypointReady) return;
// Your code here!
// ...
}
You will probably have to bind this inside your render function:
// ...
<Waypoint onEnter={this.activateWayPoint.bind(this)}/>
// ...
Alternately, instead of performing the check inside this.activateWayPoint, you might perform the check inside render:
// ...
<Waypoint onEnter={
this.state.waypointReady ?
this.activateWayPoint :
null
}/>
// ...
This assumes that your component re-renders every time you setState.
Related
So for an app, I decided to show an equation (for an app), and have real-time value updates for each value (with a button to show/hide the equation at will). So for that, not only do I have an equation component in the main app component (App.js), I also decided to put the equation component in the same file as my values component (value.js), in order to access the values and pass them as props to the equation component. I figured that I could hide the equation component in the value component, so only the equation component in the main app component is showing, and so I can also access the props at the same time, so I typed in something like this:
<Equation
className="hide"
FLoad={this.state.FLoad}
DLoad={this.state.DLoad}
DLowerBack={this.state.DLowerBack}
FTorso={this.state.FTorso}
DTorso={this.state.DTorso}
FLowerBack={this.state.FLowerBack}/>
and the "hide" class looks something like:
.hide {
display: none;
}
However, when I run my app, both show up simultaneously, and the two equations overlap each other. Why is that? Why doesn't the equation in the values component hide, even with the proper CSS tags? I hope someone can help me out. Thanks in advance.
EDIT: Here's some more code for some more context
render() {
return(
<div className="Equation">Force of Lower Back () = (-1 x (Force Load x ( Distance Load / Distance Lower Back)) + (-1 x Force Torso x (Distance Torso / Distance Lower Back))
<div>Upward Force From Legs () = Force Load + Force Torso + Force Lower Back</div></div>
);
}
}
^The code in the equation component
EDIT #2:
{equation ? <div>
<Equation/>
<button className="EquationButton" onClick = {() => equationVisibility(!equation)}>Hide Equation</button>
</div> :
<button className="EquationButton" onClick = {() => equationVisibility(!equation)}>Show Equation</button>}
<Input />
</div>
^Here's the code in the main app component as well. (The Input component being the values component I was talking about earlier)
If I understood correctly, the className="hide" is not working on your Equation component. It can be due to this Equation not using it internally. You should receive the className in the Equation and apply it manually to the internal component that needs to be hidden.
const Equation = ({ className }) => (
<div className={className} />
)
However, I would recommend you to create a prop to hide it or not, this way:
const Equation = ({ hide }) => {
if (hide) {
return null
}
return <div>{...}</div>
}
So you can use it this way:
<Equation
hide
// other props
/>
EDIT:
Based on the new piece of code that you showed, you can do this:
render() {
return(
<div className={`Equation ${this.props.className}`}> // <--- add this
Force of Lower Back () = (-1 x (Force Load x ( Distance Load / Distance Lower Back)) + (-1 x Force Torso x (Distance Torso / Distance Lower Back))
<div>
Upward Force From Legs () = Force Load + Force Torso + Force Lower Back
</div>
</div>
);
}
When you use a custom component like that you need to pass the appropriate props. Maybe you should add a className prop to the component and then add the following line:
<element className={className} />
You can use the ... spread operator to add all of the properties:
const Equation = ({...props}) => {
return (
<element {...props} />
)
}
My particular use case of React is thus:
I wish to add a small React Component to a card that is an existing, fully-functional HTML element, per all the cards on the page. This React Component shall serve to implement a new feature on those cards : reverting changes.
The HTML (well, the MVCE version of it)
is something like this:
<div id="some-id" class="card float-sm-left menu-visual-card " onclick="(function(event) { console.log('I got clicked, and a modal will spawn' ) })(event)">
<div class=card-block>
<h5 class="card-title format-text">Some title</h5>
<!-- some business elements here -->
</div>
<!-- card footer -->
<div class=customized-indicator-react></div>
</div>
The React Component
in its tl;dr version is the following:
class CustomizedIndicatorComponent extends React.Component {
constructor(props) {
super(props)
// business logic
let active = this.props.active
this.state = {
active : active
}
}
toggleActive = () => {
this.setState({
...this.state,
active : !this.state.active
})
}
// setup
componentDidMount() {
// here's where I tried to add a jQuery onclick listener to stop propagation, only to have the React Component listener get stopped
}
// teardown
componentWillUnmount() {
console.log("CustomizedIndicatorComponent destroyed!")
}
// the UI logic
render() {
if (this.state.active) {
return (
<div>
<div
className="badge badge-sm badge-info float-sm-left customized"
style={{marginRight:"10px"}}
>Customized</div>
<div
onClick={(e) => {
e.stopPropagation()
this.toggleActive()
}}
title="Click to undo customizations">
<i className="fa fa-undo" aria-hidden="true"></i>
</div>
</div>
)
}
return <div />
}
}
What happens when you run this?
When I run this, it renders. However, when I click the widget to "de-activate" the element, the container's event-handler still fires!!
I know there is a slew of internet questions about this issue or something close to it, but none of the ones I could find seem to be about this exact use case.
Also, adding an event listener in componentDidMount doesn't work, as that prevents anything from firing!
Is there any way I can make this work without wasting developer-hours refactoring everything including the parent HTMLElements?
A "hacky" way you may consider is to get the parent's id from inside the React component and disable the click event from there.
If id could not be passed as a property to the React component, you can try using ReactDOM.findDOMNode(this).parentNode.getAttribute("id") to get it and then disable the event using:
document.getElementById(id).style.pointerEvents = 'none';
I'm using Redux in my app, inside a Component I want to scroll to an specific div tag when a change in the store happens.
I have the Redux part working so it triggers the componentDidUpdate() method (I routed to this compoennt view already).
The problem as far as I can tell, is that the method scrollIntoView() doesn't work properly cos componentDidUpdate() has a default behavior that scrolls to the top overwriting the scrollIntoView().
To work-around it I wrapped the function calling scrollIntoView() in a setTimeout to ensure that happens afeterwards.
What I would like to do is to call a preventDefault() or any other more elegant solution but I can't find where to get the event triggering the 'scrollTop'
I looked through the Doc here: https://facebook.github.io/react/docs/react-component.html#componentdidupdate
and the params passed in this function are componentDidUpdate(prevProps, prevState) ,since there is no event I don't know how to call preventDefault()
I've followd this Docs: https://facebook.github.io/react/docs/refs-and-the-dom.html
And tried different approaches people suggested here: How can I scroll a div to be visible in ReactJS?
Nothing worked though
Here is my code if anyone has any tip for me, thanks
class PhotoContainer extends React.Component {
componentDidUpdate(){
setTimeout(() => {
this.focusDiv();
}, 500);
}
focusDiv(){
var scrolling = this.theDiv;
scrolling.scrollIntoView();
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>this is the div I'm trying to scroll to</div>
</div>
)
};
}
Ok it's been a while but I got it working in another project without the setTimeOut function so I wanted to answer this question.
Since Redux pass the new updates through props, I used the componentWillRecieveProps() method instead of componentDidUpdate() , this allowes you a better control over the updated properties and works as expected with the scrollIntoView() function.
class PhotoContainer extends React.Component {
componentWillReceiveProps(newProps) {
if (
this.props.navigation.sectionSelected !==
newProps.navigation.sectionSelected &&
newProps.navigation.sectionSelected !== ""
) {
this.focusDiv(newProps.navigation.sectionSelected);
}
}
focusDiv(section){
var scrolling = this[section]; //section would be 'theDiv' in this example
scrolling.scrollIntoView({ block: "start", behavior: "smooth" });//corrected typo
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>
this is the div I am trying to scroll to
</div>
</div>
)
};
}
I also struggled with scrolling to the bottom of a list in react that's responding to a change in a redux store and I happened upon this and a few other stackoverflow articles related to scrolling. In case you also land on this question as well there are a few ways this could be a problem. My scenario was that I wanted a 'loading' spinner screen while the list was rendering. Here are a few wrong ways to do this:
When loading = true, render spinner, otherwise render list.
{loading ?
<Spinner />
:
<List />
}
as stated above this doesn't work because the list you might want to scroll to the bottom of isn't rendered yet.
When loading set the display to block for the spinner and none for the list. When done loading, reverse the display.
<div style={{display: loading ? 'block' : 'none'>
<Spinner />
</div>
<div style={{display: loading ? 'none' : 'block'>
<List />
</div>
This doesn't work either since the list you want to scroll to the bottom of isn't actually being displayed likely when you call the scroll.
The better approach for the above scenario is to use a loading that acts as an overlay to the component. This way both the spinner and list are rendered and displayed, the scroll happens, and when the loading is complete, the spinner can be de-rendered or set to be invisible.
I have a drop down component that looks like this:
{...}
this.state = {
isVisible: false
}
}
toggleDisplay() {
this.setState({isVisible: !this.state.isVisible});
}
render() {
return (
<div>
<button onClick={this.toggleDisplay()}>click</button>
{this.state.isVisible ? <MenuElements toggleDisplay={this.toggleDisplay} /> : '' }
</div>
)
}
}
"MenuElements" is just a ul that has a li. On another page i am using this component multiple times, so whenever i click on the button, "MenuElements" is shown for each click. The problem is that i want only one component to be displayed. So if a MenuElements component is already displayed, if i click on another button, it closes the previous component, and opens the second one.
How could this be implemented in my code?
Thanks.
You will somehow need to have a single state that defines which MenuItem is displayed. You could go with a global state with something like Redux, but if you are trying to build a reusable component, I guess it'd be best to wrap all of the MenuItem components in a parent component and keep a state there. That, I think, is the React way of doing it. Read this for an idea of how to design components: https://facebook.github.io/react/docs/thinking-in-react.html.
BTW, I think there is an error in the Button onClick handler. It should be:
<button onClick={this.toggleDisplay.bind(this)}> // or bind it somewhere else
Also, the correct way to change state based on previous state is this:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
I'd say this is du to the context of your callbacks. Have you tried forcing the context ?
<div>
<button onClick={this.toggleDisplay.bind(this)}>
click
</button>
{this.state.isVisible ?
<MenuElements toggleDisplay={this.toggleDisplay.bind(this)} />
: '' }
</div>
I have made a component where I am rendering grids of items. On clicking one item, the item is being selected. However there are many items present so there is scroll bar. Whenever I click on an Item, the component is re-rendered (as I am putting the selectedItem in my state), which further re-renders all the other items. But when I click an item after scrolling to the bottom (or middle), the component renders to the top, however I want that to remain on the position it was being clicked.
The components are as follows :
Full-Screen (made using react-portal, contains onClick and changes its state)
--TilesView (all tiles wrapper which renders all the tiles and has an ajax call)
--all Tiles (single tile element)
The part code is as follows :
FullScreen:
componentDidMount() {
if (this.props.selectedPost) {
this.setState({
selectedPost: {
[this.props.selectedPost[0]]: true
}
});
}
}
render() {
const that = this;
//Todo: User fullpage header when space is updated
return (
<Portal container={() => document.querySelector('body')}>
<div className={styles.container}>
<FullPageForm onHide={that.props.onCancel} closeIcnLabel={'esc'} bgDark={true}>
<FullPageForm.Body>
<span className={styles.header}>{'Select Post'}</span>
<div className={styles.body}>
<ExistingAssets onCreativeTileClicked={this.handlePostClick}
selectedCreatives={this.state.selectedPost}
showSelectedTick/>
</div>
</FullPageForm.Body>
</FullPageForm>
</div>
</Portal>
);
}
handlePostClick = (adCreativeAsset, id) => {
event.preventDefault();
this.setState({
selectedPost: {
[id]: adCreativeAsset
}
});
}
In my handlePostClick, I tried doing event.preventDefault() but it didn't work. I have no clue why this is happening, thanks in advance.
Try changing your handlePostClick definition to
handlePostClick = (e, adCreativeAsset, id) => {
e.preventDefault();
//blah blah what you want
}
and in your JSX change onCreativeTileClicked={this.handlePostClick} to onCreativeTileClicked={this.handlePostClick.bind(this)}.
The event you were prevent-defaulting (stopping propagation in real terms) isn't the real event coming from the click but a synthetic one that can be summoned to fill in for an event when there isn't one. You need to stop propagation for the real event.
Hope this helps.