I'm trying to make a tag-suggest input field. I'm trying to understand why this code doesn't work, as it can be applied to any number of cases.
FYI: ReactComponent is just a helper class I implemented that contains few methods like _bind etc.
class TagSuggestInput extends ReactComponent {
constructor(){
super();
this._bind('handleSelectionClick', 'handleRemoveTag', 'addNewTag', 'render');
this.state = {
suggestedOptions: [],
tagListTo: []
};
}
addNewTag(selectedIndex){
var _this = this,
tag= _this.state.suggestedOptions[selectedIndex].tag,
tagList = _this.state.tagListTo;
if($.inArray(email, tagList) == -1){
_this.setState({tagListTO: tagList.push(tag)});
}
}
handleRemoveTag(tag){
var _this = this;
// Remove tag code goes here. This is not the problem part
}
handleSelectionClick(selectedIndex, e){
var _this = this;
_this.addNewTag(selectedIndex);
// other stuff here
}
render() {
var _this = this;
return (
<div className="tagitos">
{_this.state.tagListTo.map(function(item, index){
return (
<span key={index} >
<Tag data={item} onRemove={_this.handleRemoveTag.bind(_this)} />
</span>
);
})}
<input className="tag-input" ref="input"></input>
<ul>
{_this.state.suggestedOptions.map(function(item, index){
return (
<li key={index}
onClick={_this.handleSelectionClick.bind(_this, index)}
>
<OptionComponent data={item} index={index}/>
</li>
);
})}
</ul>
</div>
);
}
}
Child component
class Tag extends ReactComponent{
constructor(){
super();
this._bind('render', 'removeFromList');
}
removeFromList(tag){
var _this = this;
_this.props.onRemove(tag);
}
render(){
var _this = this;
return(
<span className="tag-element">
<div>{_this.props.data}</div>
<div onClick={_this.removeFromList(_this.props.data)} className="tag-closeButton">×</div>
</span>
);
}
}
I want to remove the tag by clicking on the tag X button, not on the tag itself, otherwise I could have just made the entire code in the parent scope like I did with options.
Workflow: Options are generated from back-end, as autocomplete, listed below the input field in the parent. When option is selected it generates a tag. So far so good!
BUT: the code for removing the tag is automatically called and tries to remove it. Since I've removed it the tag stays, but nothing happens on 'X' click. As if onCLick event is not bound.
Function removeFromList is not called, but it is called when component is added to the view. Why? How to prevent this? My guess is that by solving this I would solve the onClick problem also.
It doesn't work because you do not bind function to onclick. You only run it once on each render
You may write something like this
removeFromList(){
var _this = this;
var tag = _this.props.data;
_this.props.onRemove(tag);
}
...
<div onClick={_this.removeFromList}></div>
Related
I am trying to add an onClick event handler to objects in an array where the class of a clicked object is changed, but instead of only changing one element's class, it changes the classes of all the elements.
How can I get the function to work on only one section element at a time?
class Tiles extends React.Component {
constructor(props) {
super(props);
this.state = {
clicked: false,
content : []
};
this.onClicked = this.onClicked.bind(this);
componentDidMount() {
let url = '';
let request = new Request(url, {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json'
})
});
fetch(request)
.then(response => response.json())
.then(data => {
this.setState({
content : data
})
} );
}
onClicked() {
this.setState({
clicked: !this.state.clicked
});
}
render() {
let tileClass = 'tile-content';
if (this.state.clicked) {
tileClass = tileClass + ' active'
}
return (
<div className = 'main-content'>
{this.state.pages.map((item) =>
<section key = {item.id} className = {tileClass} onClick = {this.onClicked}>
<h4>{item.description}</h4>
</section>)}
<br />
</div>
)
}
class App extends React.Component {
render() {
return (
<div>
<Tiles />
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('content-app'))
You have onClicked() define in your 'main-content' class. So that's where it fires.
constructor(props) {
// super, etc., code
this.onClicked = this.onClicked.bind(this); // Remove this line.
}
Remove that part.
You can keep the onClicked() function where it is. Your call in render() is incorrect, though: onClick = {this.onClicked}>. That accesses the onClicked ATTRIBUTE, not the onClicked FUNCTION, it should be this.onClicked().
Let me cleanup your call in render() a little bit:
render() {
let tileClass = 'tile-content';
return (
<div className = 'main-content'>
// some stuff
<section
key={item.id}
className={tileClass}
onClick={() => this.onClicked()} // Don't call bind here.
>
<h4>{item.description}</h4>
</section>
// some other stuff
</div>
)
}
It is happening for you, because you are assigning active class to all sections once user clicked on one of them. You need somehow to remember where user clicked. So I suggest you to use array, where you will store indexes of all clicked sections. In this case your state.clicked is an array now.
onClicked(number) {
let clicked = Object.assign([], this.state.clicked);
let index = clicked.indexOf(number);
if(index !== -1) clicked.splice(index, 1);
else clicked.push(number)
this.setState({
clicked: clicked
});
}
render() {
let tileClass = 'tile-content';
return (
<div className = 'main-content'>
{this.state.pages.map((item, i) => {
let tileClass = 'tile-content';
if(this.state.clicked.includes(i)) tile-content += ' active';
return (
<section key = {item.id} className = {tileClass} onClick = {this.onClicked.bind(this, i)}>
<h4>{item.description}</h4>
</section>
)
})}
<br />
</div>
)
}
StackOverflow does a particularly poor job of code in comments, so here's the implementation of onClicked from #Taras Danylyuk using the callback version of setState to avoid timing issues:
onClicked(number) {
this.setState((oldState) => {
let clicked = Object.assign([], this.state.clicked);
let index = clicked.indexOf(number);
if(index !== -1) {
clicked.splice(index, 1);
} else {
clicked.push(number);
}
return { clicked };
});
}
The reason you need this is because you are modifying your new state based on the old state. React doesn't guarantee your state is synchronously updated, and so you need to use a callback function to make that guarantee.
state.pages need to keep track of the individual click states, rather than an instance-wide clicked state
your onClick handler should accept an index, clone state.pages and splice your new page state where the outdated one used to be
you can also add data-index to your element, then check onClick (e) { e.currentTarget.dataset.index } to know which page needs to toggle clickstate
Good afternoon,
I am having trouble changing the state of components which are siblings. Basically they look like this:
<Navigation>
<NavEndpt /> About <--- no
<NavEndpt /> Blog
<NavEndpt /> Projects
</Navigation>
Each <NavEndpt /> is a link that routes to a different 'page' or route on my website unless it is the first one listed, "About". About I want to be a drop down/lightbox feature something pretty to show off my CSS knowledge. So when one of the links is clicked I want it to go where I want; additionally, I'd like it to reflect which link is currently "active" (or not) by both changing it's state.active to true or false and adding an additional class active for CSS purposes. I want to new "active" class to only be present on a <NavEndpt /> that's state.active is true.
Yet everything I have tried so far hasn't worked and I've been at this for two days. I'd appreciate someone who is more experienced with React to show me how to accomplish this.
Here is what I am working with:
var MasterLayout = React.createClass({
mixins: [History],
render: function(){
var childrenWithProps = React.Children.map(this.props.children,
(child) => React.cloneElement(child, {
doSomething: this.doSomething
})
);
return(
<div id="container">
<Navigation activeRoute={this.props.location.pathname} />
<div id="content">
{childrenWithProps}
</div>
</div>
)
}
});
var Navigation = React.createClass({
getInitialState: function(){
var endpoints = require("./data/naviEnd.js");
return {
endpoints: endpoints,
activeRoute: this.props.activeRoute
}
},
renderEndpoints: function(key){
var endpointDetails = this.state.endpoints[key];
return(
<NavEndpt id={endpointDetails.id} key={endpointDetails.title} url={endpointDetails.url} title={endpointDetails.title}/>
)
},
render: function(){
return(
<div id="navigation">
{Object.keys(this.state.endpoints).map(this.renderEndpoints)}
</div>
)
}
});
// Created child not a this.props.child of <Navigation /> component
// as pointed out by #BenHare
var NavEndpt = React.createClass({
handleClick: function(){
this.setState({
active: true
})
},
render: function(){
return (
<div onClick={this.handleClick} className="navLink" id={this.props.id}>
<Link id={this.props.id + "-link"} to={this.props.url}>{this.props.title}</Link>
</div>
)
}
})
Currently this only changes creates and set states for each <NavEndpt /> I tried to make this mess as clean as possible for Stack Overflow.
The best fix I have come up with so far uses a lot of DOM selection and hardcoded if/else statements. It also doesn't light up my "About" component because it doesn't have a url property. That's significant because I have the below solution tied up to the pathname of my entire layout component.
var MasterLayout = React.createClass({
mixins: [History],
render: function(){
var childrenWithProps = React.Children.map(this.props.children,
(child) => React.cloneElement(child, {
doSomething: this.doSomething
})
);
return(
<div id="container">
<Navigation activeRoute={this.props.location.pathname} />
<div id="content">
{childrenWithProps}
</div>
</div>
)
}
});
// This is the parent component that sits on the side or the top depending on the
// broswer size, contains components NavEndpt
var Navigation = React.createClass({
getInitialState: function(){
var endpoints = require("./data/naviEnd.js");
return {
endpoints: endpoints,
activeRoute: this.props.activeRoute
}
},
// Always makes the website's initial view the home route
componentDidMount: function(){
var cover = document.getElementById("cover");
var projects = document.getElementById("projects");
var about = document.getElementById("about");
var active = this.props.activeRoute
this.setActive();
},
// resets the hard coded CSS class
resetClasses: function(){
var active = this.props.activeRoute
var cover = document.getElementById("cover");
var projects = document.getElementById("projects");
var about = document.getElementById("about");
cover.className = "navLink";
projects.className = "navLink";
about.className = "navLink";
},
// checks pathname of <MasterLayout/>
// also somehow makes it so a refresh does not
// return you to "/"
setActive: function(){
var active = this.props.activeRoute
var cover = document.getElementById("cover");
var projects = document.getElementById("projects");
var about = document.getElementById("about");
if (active === "/"){
cover.className += " active";
} else if (active === "/projects"){
projects.className += " active"
} else if (active === "/about"){
about.className += " active"
}
},
// listens for updates, resets active first and sets it
componentDidUpdate: function(){
this.resetClasses();
this.setActive();
},
renderEndpoints: function(key){
var endpointDetails = this.state.endpoints[key];
return(
<NavEndpt id={endpointDetails.id} key={endpointDetails.title} url={endpointDetails.url} title={endpointDetails.title}/>
)
},
render: function(){
return(
<div id="navigation">
{Object.keys(this.state.endpoints).map(this.renderEndpoints)}
</div>
)
}
});
var NavEndpt = React.createClass({
handleClick: function(){
this.setState({
active: true
})
},
render: function(){
return (
<div onClick={this.handleClick} className="navLink" id={this.props.id}>
<Link id={this.props.id + "-link"} to={this.props.url}>{this.props.title}</Link>
</div>
)
}
})
I have a trigger that is supposed to change a state of its child component. The results of both render statements are inconsequential. My only concern here is that I am unsure how to use the trigger trigger to call the leftbehind function without putting leftbehind inside its parent render Another.
My code is below. The goal is to have leftbehind run without having to put it inside the render.
var Another = React.createClass({
leftbehind: function() {
if (this.props.status === "dare") {
alert('Winning!');
}
},
render: function() {
if (this.props.status === "truth") {
return (<p>Yes</p>);
} else {
return (<p>Nope</p>);
}
}
});
var App = React.createClass({
getInitialState:function() {
return {deesfault: "truth"};
},
trigger: function() {
this.setState({deesfault: "dare"});
},
render: function() {
return (
<div>
<p onClick={this.trigger}>{this.state.deesfault}</p>
<Another status={this.state.deesfault}/>
</div>
);
}
});
The reason I do not want to place leftbehind inside the render, is because it is technically supposed to take the place of an API call. And I do not want that to be called inside the render function.
Your implementation executes leftbehind each time <Another> is rendering with its status prop being true. That said, once status is flipped to true, leftbehind will be executed over and over in every following rendering until it is flipped back to false. This will seriously cause problems.
Since the intention is to trigger leftbehind with a click event, I would restructure the components in different ways.
Move leftbehind into the parent component and have it executed along with the click event. If <Another> needs the results, passed them on through props.
var Another = React.createClass({
render() {
return <div>{this.props.params}</div>;
}
});
var App = React.createClass({
getInitialState() {
return {apiRes: null};
},
onClick() {
const res = someAPICall();
this.setState({apiRes: res});
},
render() {
return (
<div>
<p onClick={this.onClick}>Fire</p>
<Another params={this.state.apiRes} />
</div>
);
}
});
Or, move the <p> element into <Another> along with the click event.
var Another = React.createClass({
getInitialState() {
return {apiRes: null};
},
onClick() {
var res = someAPICall();
this.setState({apiRes: res});
},
render() {
return (
<div>
<p onClick={this.onClick}>Fire</p>
<div>{this.state.apiRes}</div>
</div>
);
}
});
var App = function() { return <Another />; }
In the latter, the key logic is handled in the inner component. The outer one is just a container. In the former one, the outer component handles the logic and pass on the results if any. It depends on how the components relate with the API call to decide which suits better. Most importantly, in both cases the API will not execute unless the click event is triggered.
I'm working with react on an app that has an hidden to style and input and simulate a click when the styled input has focus
class UploadComponent extends React.Component {
render() {
return (
<div className="row">
<input type="file" className='hidden' ref="fileChooser"/>
<input type="text" className="input-file" onFocus={this.onFocus.bind(this)} ref="fileName" />
</div>
);
}
onFocus() {
this.refs.fileChooser.getDOMNode().click();
}
}
it's working correctly on Chrome and Safari but not on Firefox.
I've found that it may be a bug on firefox,
do you know if i'm doing something wrong?
For security reasons, Firefox suppresses invoking HTMLInputElement.click() other than from a button-click callback. You even can't use react its synthetic eventsystem (onClick props), for you would be outside the original click-event-subscriber. The only way to get this work, is by subscribe using the native way like this:
The next codesnap should work. Or feel free to use this react-component: https://www.npmjs.com/package/itsa-react-fileuploadbutton
class UploadComponent extends React.Component {
render() {
return (
<div className="row">
<input type="file" className='hidden' ref="fileChooser"/>
<button ref="selectBtn" />Select files</button>
</div>
);
}
componentDidMount() {
const instance = this; // `this` doesn't get minified, `instance` does
instance.handleClick = instance.handleClick.bind(instance);
instance._inputNode = ReactDOM.findDOMNode(instance.refs.fileChooser);
instance._buttonNode = ReactDOM.findDOMNode(instance.refs.selectBtn);
instance.IE8_Events = !instance._buttonNode.addEventListener;
if (instance.IE8_Events) {
instance._buttonNode.attachEvent('onclick', instance.handleClick);
}
else {
instance._buttonNode.addEventListener('click', instance.handleClick, true);
}
}
componentWillUnmount() {
const instance = this; // `this` doesn't get minified, `instance` does
if (instance.IE8_Events) {
instance._buttonNode.detachEvent('onclick', instance.handleClick);
}
else {
instance._buttonNode.removeEventListener('click', instance.handleClick, true);
}
}
handleClick() {
this._inputNode.click();
}
}
I have a form that displays an error container if any errors are added to the state. There is a handleSubmit function that calls a validateForm function before sending through the ajax request.
getInitialState: function () {
return {
formErrors: []
};
},
render: function () {
var errors = this.state.formErrors.map(function(error) {
return (
<li>
{error}
</li>
);
});
var errorContainer = (
<ul>
{errors}
</ul>
);
In the return block of the render function, there is this line to display the error container:
{ this.state.formErrors.length == 0 ? '' : errorContainer }
Inside of the validateForm function, I have something like this:
this.setState({ formErrors: [] });
var name = this.refs.name.getDOMNode().value.trim();
if (!name) {
newErrors = this.state.formErrors;
newErrors.push('Please add a name');
this.setState({
formErrors: newErrors
});
}
This works in that it will add the errors to the formErrors key in the state, but if I then input the correct things, any "new" errors will be appended in the view to the li as opposed to painting a fresh error container with N-1 errors, as in that scenario I would have filled in the name field.
Here's a jsfiddle: http://jsfiddle.net/ke4bmqny/7/
Your parent form component controls the error state, when you pass this down to the error display child component the errors are props of that component. See Communicate Between Components.
Change this line to camel-case :P
<GroupFormErrorComponent formErrors={this.state.formErrors} />
And you had it almost right but your error display component doesn't and shouldn't need to copy the error into another state variable.
var GroupFormErrorComponent = React.createClass({
render: function () {
var errors = this.props.formErrors.map(function(error, index) {
return (
<li key={index}>
{error}
</li>
);
});
var errorContainer = (
<div>
<div>
Sorry but there are errors:
</div>
{errors}
</div>
);
return (
<div>
{ this.props.formErrors.length == 0 ? '' : errorContainer }
</div>
);
}
});
PS Just a little tip on the DOMReady stuff you added to js fiddle - you didn't need it, instead just put the <script> part below the html (below <div id='form'></div>) and it will be ready.
Updated fiddle: http://jsfiddle.net/dL0uj9oc/3/
As far as I can see there is no case where 'this.state.formErrors' is emptied. So I would change the lines to:
var newErrors = [];
if (!name) {
newErrors.push('Please add a name');
this.setState({
formErrors: newErrors
});
}
But it would be better if you shared a jsfiddle where people can reproduce the problem.
On a side note, may be you should get your name value from state, instead of reading from DOM (where you get the name input value).