React Component "closing" when lost focus - javascript

I'm trying to create a select2 style component in React.
I have got 90% functionality down, the one bit I just can't fathom is hiding the result box when the user clicks away
The render method is:
render() {
let resultBlock;
if (this.state.showSearch) {
resultBlock = (
<div className="search-input-container" onBlur={this.onBlur}>
<div className="search-input-results">
<input
type="text"
name={this.props.name}
placeholder={this.props.placeholder}
className="form-control"
onChange={this.inputKeyUp}
autoComplete="false" />
<ul>
{this.state.items.map((item, i) => <li key={i} data-value={item.id} onClick={this.itemSelected} className={item.isSelected ? 'selected' : ''}>{item.text}</li>)}
</ul>
</div>
</div>
);
}
let displayBlock;
if (this.props.value.text) {
displayBlock = this.props.value.text;
} else {
displayBlock = <span className="placeholder">{this.props.placeholder}</span>;
}
return (
<div className="form-group">
<label htmlFor={this.props.name}>{this.props.label}:</label>
<div className="form-input">
<div className="searchable-dropdown" onClick={this.revealSearch}>
{displayBlock}
<div className="arrow"><i className="fa fa-chevron-down" aria-hidden="true" /></div>
</div>
{resultBlock}
</div>
</div>
);
}
I've tried moving onBlur={this.onBlur} around, but it only fires if the <input... had focus before one clicked away.
It can't be that complicated, the only approach I thought of, is attaching a global click handler to the page, and diff'ing clicks to understand if a user hasn't clicked on my component. But this seems over engineered.
How can this be achieved?

I achieved this functionality by:
Putting this in the constructor:
this.windowClick = this.windowClick.bind(this);
(From what dfsq said) Put this in componentDidMount:
if (window) {
window.addEventListener('click', this.windowClick, false);
}
This event handler:
windowClick(event) {
event.preventDefault();
if (event.target.classList.contains('searchable-marker')) {
return;
} else {
this.setState({
showSearch: false
});
}
}
Where searchable-marker is just a class I put on all the div's, ul's, li's and inputs to make sure that if I clicked one of these, it wouldn't close the the box.
Adding the unmount:
componentWillUnmount() {
window.removeEventListener('click', this.windowClick, false);
}

what you can try doing is onBlur you could change the value of this.state.showSearch = false and when this condition is satisfied add a className="hide" (hide{display: none}) by creating a custom function which returns a classname as a string.

Related

OnClick event not working. Is there something wrong with my event handler?

I have this piece of code, I want to show replies after the click event but it doesn't seem to fire or render.
// function for rendering comment's replies
const renderReplies = (comment) =>{
if(comment.data.replies){
return (
<div className="reply">
{comment.data.replies.data.children.map(reply => {
if(reply.data.body){
return (
<div className="comment-card">
<span className="comment-author">{reply.data.author}</span>
<ReactMarkdown>{reply.data.body}</ReactMarkdown>
<div className="upvotes">
<ImArrowUp id="comment-arrow-icon" />
{reply.data.score}
</div>
{showMoreReplies(reply)}
</div>
)
}
})}
</div>
)}}
const showMoreReplies = (reply) => {
if(reply.data.replies){
return (
<span className="show-all-replies" onClick={() => renderReplies(reply)} >Show more replies <HiArrowNarrowRight className="show-more-replies-arrow" /> </span>
)
}
}
If I execute renderReplies() within itself like this:
</div>
{renderReplies(reply)}
</div>
then the replies show up properly. The only issue is that I want them to show up after click event. I could use display: none but since I have a lot of comments with their own replies it will be a bit complicated

Mouse hover in React

I'm trying to display a div when the mouse is over another div element. I've managed to do so via onMouseEnter and onMouseLeave.
The issue here is that if you quickly move from one div to another (it's an array of divs that contain data about a product), the value of index[0] becomes true.
The way it works is that I have an array initialised to false when the mouse enters one of them, it becomes true and shows the div that I wanted. Once it leaves, it set it back to false.
this.state = {
isProductsHovering: new Array(this.props.currentProducts.length).fill(false)
};
handleMouseHover = (idx) => {
this.setState({
isProductsHovering: update(this.state.isProductsHovering, {
[idx]: { $set: !this.state.isProductsHovering[idx] }
})
})
}
render() {
return this.props.currentProducts.map((product, idx) => {
return <Fragment key={idx}>
<div className="product-grid-view col-6 col-md-4" >
<div
className=" product-holder"
onMouseEnter={this.handleMouseHover.bind(this, idx)}
onMouseLeave={this.handleMouseHover.bind(this, idx)}>
<div className="image-container" align="center">
<img src={"/img/product-3.jpg"} alt="" />
{
this.state.isProductsHovering[idx] &&
<div className="product-buttons">
<Link to={`products/${product.id}`} className="btn-detail" text="View Details" />
<Link to='#' className="btn-cart" icons={["icon-cart", "icon-plus"]} />
</div>
}
</div>
<div className="details-holder">
<span className="part-text">{product.desc}</span><br />
<span className="manufacturer-text">{product.manufacturer.name}</span>
<div className="product-review_slide">
<Stars values={product.averageRating} {...starsRating} />
<span className="product-review">{getLength(product.reviews)} review</span>
</div>
<span className="product-price">{product.salesPrice.toFixed(2)}</span>
<span className="product-currency">SR</span>
</div>
</div>
</div>
</Fragment>
})
}
Update
I've made a stackblitz project to reproduce the same issue as suggested:
https://stackblitz.com/edit/react-mouse-hover.
For everyone that wants to see what I mean. I've attached a photo of the issue. If you move the mouse over the two divs (up and down as quick as you can), this what happens:
mouse hover broken
For situation like this, I wouldn't rely on array and index to make it work. You are further complicating your handleMouseHover functions and the checking of isHovering.
A 'more React' way of dealing with this situation is simply make each Product a component itself. And this Product component will have its own state of isHovered and handleOnHover method, that way you create a more concise and reliable code without having to rely on array index at all:
App.js can be as simple as this:
class App extends Component {
render() {
return (
<div>
{
data.map(product =>
<Product product={product} />
)
}
</div>
)
}
}
A new Product.js:
import React from 'react'
import ReactHoverObserver from 'react-hover-observer';
export default class Product extends React.Component {
render() {
const { product } = this.props
return (
<ReactHoverObserver className="product-grid-view col-6 col-md-4">
{
({isHovering}) => (
<div className=" product-holder">
<div className="image-container" align="center">
<img src={"/img/product-3.jpg"} alt="" />
{
isHovering &&
<div className="product-buttons">
<button className="btn-detail">View Details</button>
</div>
}
</div>
<div className="details-holder">
<span className="part-text">{product.desc}</span><br />
<span className="manufacturer-text">{product.manufacturer.name}</span>
<div className="product-review_slide">
<span className="product-review">0 review</span>
</div>
<span className="product-price">{product.salesPrice.toFixed(2)}</span>
<span className="product-currency">Currency</span>
</div>
</div>
)
}
</ReactHoverObserver>
)
}
}
I have put the moficiation in Stackblitz: https://stackblitz.com/edit/react-mouse-hover-2cad4n
Liren's answer is good advice and will help simplify the code. One thing I also noticed is that occasionally the HoverObserver won't 'hear' an event, and since the hover enter and hover exit events are listening to the same event, then the display state for the button will become reversed (i.e., it will show when the mouse is NOT hovering and hide when the mouse hovers over the observer).
I would recommend getting rid of the ReactHoverObserver HOC and instead just listen for the onMouseOver for hover enter and onMouseLeave for hover exit. That way, even if the div doesn't register a hover enter or exit, it will easily reset because onMouseOver will toggle the display state to true and onMouseLeave will reliably set the button's display state to false.
See here for those events in the docs:
https://reactjs.org/docs/events.html#mouse-events
The way you trigger it (from array or from a component) is semantics , the real issue is that these events don't always fire.
I had the same issue , apparently the events of react are not that reliable.
In my case I could live in a situation where a tooltip does not close , but not in the situation where 2 tooltips are open. In order to solve my issue , I returned to good old dom manipulation - Every time a tooltip appeared , it made all the other ones invisible.
It looked something like this :
showTooltip() {
// Clear old tooltips, EventTooltip is the name of my tooltip class.
Array.from(document.getElementsByClassName('EventTooltip'))
.forEach(tooltip=>tooltip.style = 'display:none;')
this.setState({showTooltip: true})
}
hideTooltip() {
this.setState({showTooltip: false})
}

button value passing as undefined

I apologize for the simple question, I'm new to react and I have yet to find a solution for this problem. When clicked, I'm trying to retrieve the value for each button. The value repeatedly comes back as undefined. What am I doing wrong? I greatly appreciate your help!
class BottomListFilter extends Component {
constructor(props) {
super(props);
this.handleButton = this.handleButton.bind(this);
}
handleButton = (evt) => {
console.log(evt.target.value);
};
render() {
const listFilter = this.props.initialAdvisorList.map((filter) =>
<li key={filter.id}>
{filter.location}
</li>
);
return(
<div>
<div>
<button
value='location'
onClick={this.handleButton}
>
<h2>Choose by location</h2>
</button>
<button
value='state'
onClick={this.handleButton}
>
<h2>Choose by state</h2>
</button>
<button
value='practice'
onClick={this.handleButton}
>
<h2>Choose by practice</h2>
</button>
<button
value='topic'
onClick={this.handleButton}
>
<h2>Choose by topic</h2>
</button>
</div>
<div>
<ul>
{listFilter}
</ul>
</div>
</div>
);
}
}
You can certainly do evt.target.parentNode.value and it will work in this case. It may be just me but I find that solution against the spirit of React as it's reminisce of traversing the DOM (JQuery days).
Passing the value to handleButton function is much cleaner and more flexible:
<button onClick={this.handleButton.bind(null, 'location')}>
<h2>Choose by location</h2>
</button>
Now your handleButton looks like this:
handleButton(value, event) {
console.log(value);
console.log(event);
}
In this case, you don't really need "event" anymore and you could easily do handleButton(value) but I have included it just for the sake of clarity.
you can simplify your handleButton situation. it doesn't need to be a method. (in the code below, i also stripped away the ul, just to isolate the issue.)
i don't think this is what was causing the problem, though. most likely, the problem was not a React issue but just a simple DOM issue. event.target means "the lowest-level node where the event took place". if you clicked on the h2, that node is the h2, which has no value property.
just don't use an h2. put the text directly inside the button. also shown in the code sample.
var handleButton = evt => {
console.log(evt.target.value)
}
class BottomListFilter extends Component {
constructor(props) {
super(props);
this.handleButton = this.handleButton.bind(this);
}
render() {
return(
<div>
<button
value='location'
onClick={handleButton}
>
Choose by location
</button>
<button
value='state'
onClick={handleButton}
>
Choose by state
</button>
<button
value='practice'
onClick={handleButton}
>
Choose by practice
</button>
<button
value='topic'
onClick={handleButton}
>
Choose by topic
</button>
</div>
);
}
}

Don't trigger onClick for nested child elements

I have a simple modal component:
function Modal(props) {
return (
<div className={cx(styles.overlay, { show: props.show })} onClick={props.onClose}>
<div className={styles.modal}>
<span className={styles.closeBtn} onClick={props.onClose} />
{props.children}
</div>
</div>
)
}
the onClose prop triggers closing the modal, hence I attach it to styles.overlay (dark overlay that you typically see on modals that when clicked dissmises it) and to styles.closeBtn (a close button for modal).
The whole flow works besides the fact that anything inside styles.overlay when clicked on also dismisses the modal, which is not functionality I was after, hence I need to only dismiss it if that specific element is clicked not its children.
function Modal(props) {
return (
<div className={cx(styles.overlay, { show: props.show })} onClick= {props.onClose}>
<div className={styles.modal} onClick={e => e.preventDefault()}>
<span className={styles.closeBtn} onClick={props.onClose} />
{props.children}
</div>
</div>
)
}
I think, the best way is to have your overlay and your modal in two separate div, but this should work.
Add onClick(e)={e.stopPropagation();} to the modal dialog's click handler; this should prevent it from propagating to the overlay.
Hope it works! Good luck!

Why is not this onClick work?

I have the following reactJS/JSX code :
var LikeCon = React.createClass({
handleClick: function(like) {
return;
},
render(){
return this.renderLikeButton(this.props.like, this.props.likeCount)
},
renderLikeButton(like, likeCount){
return (
content = <div className={like==true ? "likeButConAct" : "likeButCon"}>
<div className="likeB" onClick={this.handleClick(!like)} > </div>
{ likeCount > 0 ? <div className="likeCount">{likeCount}</div>: null}
</div>
);
}
});
The problem is that handleClick will never be triggered even when I click the likeB div? Why?
Edit :
This is the code that uses the LikeCon component :
var TopicComments = React.createClass({
render: function() {
var comment = this.props.data.map(function(com, i) {
return (
<article>
<div className="comment">
<div className="tUImgLnk">
<a title={com.UserName} target="_blank" href={com.UserInfoUrl}>
<img className="tUImg" src={com.UserPicSrc} />
</a>
</div>
{com.UserName}
<div className="content">
{com.Message}
</div>
<div className="status">
<div className="dateCreated dimText">
{com.DateCreated}
</div>
<LikeCon like={com.Like} likeCount={com.LikeCount} />
<article></article>
</div>
</div>
</article>);
}.bind(this));
return(
<div className="comments">
{comment}
</div>
);
}
});
I suspect the problem is that the LikeCon is generating a markup for the TopicComment so the handleClick is not really there when triggered from the TopicComment. Is there a simple way to fix this?
You should be passing handle click event like so:
<div className="likeB" onClick={this.handleClick.bind(this,!like)} > </div>
With your current version you are passing result of executing this.handleClick(!like) to onClick handler which is undefined.
With above version you are passing a function which takes !like as its first parameter when executed.
Update:
Also since your div only contains a single space character, it is difficult to find the space character and click on it. If you add a text and click on that text, you will see the handle function is being executed:
working fiddle : http://jsfiddle.net/an8wvLqh/

Categories