Basically what my code does is display a table list of names with a search bar at the top that filters the list as you type in a value.
The problem I am having with my code at the moment is adding an if statement to the DisplayTable component. I dont want it to display all the stored data but just display the ones that have been inputted by the user in the search bar {queryText}
Please Ignore the tableData variable
var InstantBox = React.createClass({
doSearch:function(queryText){
console.log(queryText)
//get query result
var queryResult=[];
this.props.data.forEach(function(person){
if(person.name.toLowerCase().indexOf(queryText)!=-1)
queryResult.push(person);
});
this.setState({
query:queryText,
filteredData: queryResult
})
},
getInitialState:function(){
return{
query:'',
filteredData: this.props.data
}
},
render:function(){
return (
<div className="InstantBox">
<h2>Who is Richer?</h2>
<SearchBox query={this.state.query} doSearch={this.doSearch}/>
<DisplayTable data={this.state.filteredData}/>
</div>
);
}
});
var SearchBox = React.createClass({
doSearch:function(){
var query=this.refs.searchInput.getDOMNode().value; // this is the search text
this.props.doSearch(query);
},
render:function(){
return <input className="searchbar-edit" type="text" ref="searchInput" placeholder="Search Name" value={this.props.query} onChange={this.doSearch}/>
}
});
var DisplayTable = React.createClass({
doSearch:function(queryText){
console.log(queryText)
//get query result
var queryResult=[];
this.props.data.forEach(function(person){
if(person.name.toLowerCase().indexOf(queryText)!=-1)
queryResult.push(person);
});
this.setState({
query:queryText,
filteredData: queryResult
})
},
render:function(){
//making the rows to display
var rows=[];
this.props.data.forEach(function(person) {
rows.push(<tr><td>{person.image}</td></tr>)
rows.push(<tr><td>{person.name}</td></tr>)
});
//returning the table
return(
<table>
<tbody>{rows}</tbody>
</table>
);
}
});
var tableData=[
{
name:'Paul mak',
image: <img width="50" src="./images/profile_img.png"/>,
},
];
var dataSource=[
{
name:'Paul mak',
image: <img width="50" src="./images/profile_img.png"/>,
},
{
name:'John Doe',
image : '002'
},
{
name:'Sachin Tendulkar',
image : '003'
}];
React.render(
<InstantBox data={dataSource}/>,
document.getElementById('content1')
);
Try something like this:
var InstantBox = React.createClass({
doSearch:function(queryText){
console.log(queryText)
//get query result
var queryResult=[];
this.props.data.forEach(function(person){
if(person.name.toLowerCase().indexOf(queryText)!=-1)
queryResult.push(person);
});
this.setState({
query:queryText,
filteredData: queryResult
})
},
getInitialState:function(){
return{
query: '',
filteredData: undefined
}
},
renderResults: function() {
if (this.state.filteredData) {
return (
<DisplayTable data={this.state.filteredData}/>
);
}
},
render:function(){
return (
<div className="InstantBox">
<h2>Who is Richer?</h2>
<SearchBox query={this.state.query} doSearch={this.doSearch}/>
{this.renderResults()}
</div>
);
}
});
What I've changed from your code is I changed this.state.filteredData to be undefined (in fact you could just remove it entirely, but I thought this was clearer for you right now) in your initial state. This way when you first render the box, there's no filteredData and your <DisplayTable /> doesn't render. As soon as you run your doSearch callback from <SearchBox /> it will populate filteredData and display it.
To extend this you could also check when this.state.query is undefined again or blank (eg with : this.state.query.length) to remove <DisplayTable /> from the dom again if there is no query / no results.
Remember render functions are still just javascript. Anything you wrap in {} inside JSX will be evaluated. We could have just had this logic inside the render function and be doing something like var displayTable = <DisplayTable />; and then including {displayTable} in the returned JSX and it would have been the same. I personally just prefer splitting render logic up among different functions :)
Related
Learning React. Attempting to make my own mini-app based very closely on what's done here: https://www.youtube.com/watch?v=-AbaV3nrw6E.
I'm having a problem with the deletion of comments in my app. I've looked in several other places for people having similar errors, but it seems the problem is within my own code (and yet I can find no errors). I've scoured the Babel file over and over, but to no avail.
Here are the specifics:
When you create a new comment, you have two options in the form of buttons: Save and Delete. After exactly one comment is written and you press "Save," the delete function works just fine. However, if there are three comments total (for example) and you click "delete" on the first one, the next comment (the second, in this case) is deleted.
Hopefully that makes some amount of sense.
Can you find my error? The math/logic behind the delete function is located on line 71 under the name "deleteComment."
Full Pen here.
var CommentSection = React.createClass({
getInitialState: function() {
return {editing: true}
},
edit: function() {
this.setState({editing: true});
},
save: function() {
this.props.updateCommentText(this.refs.newText.value, this.props.index);
this.setState({editing: false});
},
delete: function() {
this.props.deleteFromCard(this.props.index);
},
renderNormal: function() {
return (
<div className="comment-section">
<div className="comment-content">{this.props.children}</div>
<a className="-edit" onClick={this.edit}>Edit</a>
</div>
);
},
renderEdit: function() {
return (
<div className="comment-section">
<textarea ref="newText" defaultValue={this.props.children}></textarea>
<button className="-save" onClick={this.save}>Save</button>
<button className="-delete" onClick={this.delete}>Delete</button>
</div>
);
},
render: function() {
if(this.state.editing) {
return this.renderEdit();
} else {
return this.renderNormal();
}
}
});
var PhotoSection = React.createClass({
render: function() {
return <div className="photo-section"></div>;
}
});
var Desk = React.createClass({
getInitialState: function() {
return {
comments: []
}
},
addComment: function(text) {
var arr = this.state.comments;
arr.push(text);
this.setState({comments: arr})
},
deleteComment: function(i) {
console.log(i);
var arr = this.state.comments;
arr.splice(i, 1);
this.setState({comments: arr})
},
updateComment: function(newText, i) {
var arr = this.state.comments;
arr[i] = newText;
this.setState({comments: arr})
},
commentFormat: function(text, i) {
return (
<CommentSection key={i} index={i} updateCommentText={this.updateComment} deleteFromCard={this.deleteComment}>
{text}
</CommentSection>
);
},
render: function() {
return (
<div className="desk">
<div className="card">
<PhotoSection />
<div className="comment-section-backing">
<button className="-comment" onClick={this.addComment.bind(null, "")}>Leave a Comment</button>
{this.state.comments.map(this.commentFormat)}
</div>
</div>
</div>
);
}
});
ReactDOM.render(<Desk />, document.getElementById('app'));
Your problem stems from using the index as keys:
https://facebook.github.io/react/docs/lists-and-keys.html#keys
When you delete an item from your array, the array in the state is correctly updated. However, when the array is rendered, they keys will all be the same regardless of which element you deleted except there will be one less.
At this point the reconciliation happens and your components are rerendered. However you have an (uncontrolled) textarea in each component that holds its own internal state. The uncontrolled textarea component does get it's default (initial) value from the children prop but is otherwise unaffected by changes to that value. Therefore the re-rendering of the components with new values for text do not change the values in those textarea instances.
If the keys for the components in the mapped components were not linked to the index, the correct component would be removed.
edit: The code in the pen has changed slightly where there are two different render branches (editing, normal). Since the normal rendering doesn't use the uncontrolled textarea inputs the pen no longer exhibits the aberrant behavior.
There was an issue with using this.props.children when rendering the CommentSection component
Changing the code to use a prop:
return (
<div className="comment-section">
<div className="comment-content">{this.props.commentText}</div>
<a className="-edit" onClick={this.edit}>Edit</a>
<button className="-delete" onClick={this.delete}>Delete</button>
</div>
);
and setting this in the commentFormat functiion in the container:
commentFormat: function(text, i) {
return (
<CommentSection
key={i}
index={i}
updateCommentText={this.updateComment}
deleteFromCard={this.deleteComment}
commentText={text}>
</CommentSection>
);
}
appears to work.
CodePen
Try with Array.filter.
deleteComment: function(i) {
var arr = this.state.comments.filter(function(comment) {
return comment.index !== i;
});
this.setState({comments: arr});
},
I've made this "Currency-Converter" to get an idea of how React works.
It works (more or less) but the result is shown with an offset:
You type "1" (Euro) => It shows "0 Dollar".
You type "10" => It shows "1.1308 Dollar".
You type "100" => It shows "11.308 Dollar".
...
var Display = React.createClass({
render: function() {
return (
<div>
<p>{this.props.euro + ' Euro are equal to ' + this.props.dollar + ' Dollar.'}</p>
</div>
)
}
});
var Converter = React.createClass({
getInitialState: function() {
return { euro: 0, dollar: 0, exchangeRate: 1.1308 }
},
convertEuroToDollar: function() {
this.setState({ euro: +document.querySelector('#amount-euro').value });
this.setState({ dollar: this.state.euro * this.state.exchangeRate });
},
render: function() {
return (
<div>
<input type="text" id="amount-euro" onKeyUp={this.convertEuroToDollar} />
<Display dollar={this.state.dollar} euro={this.state.euro} exchangeRate={this.state.exchangeRate} />
</div>
)
}
});
ReactDOM.render(
<Converter />,
document.querySelector('#app')
);
div {
margin: 30px 50px;
}
<div id="app"></div>
Live-Demo on CodePen: http://codepen.io/mizech/pen/vGbJxe
It should display the result (euro * exchangeRate) at once.
What I'm doing wrong here?
Calling two setStates one after all, you wasn't setting the euro state properly.
Being async, you was still using the old value of it.
From the docs:
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value.
https://facebook.github.io/react/docs/component-api.html
To fix the problem, do:
convertEuroToDollar: function() {
const euro = +document.querySelector('#amount-euro').value
this.setState({
euro: euro,
dollar: euro * this.state.exchangeRate
});
},
Fixed example: http://codepen.io/FezVrasta/pen/xVeMMX
Second problem I see, it would be much better to use ref instead of document.querySelector.
convertEuroToDollar: function() {
const euro = +this.refs.amountEuro.value;
this.setState({
euro: euro,
dollar: euro * this.state.exchangeRate
});
},
render: function() {
return (
<div>
<input type="text" ref="amountEuro" onKeyUp={this.convertEuroToDollar} />
<Display dollar={this.state.dollar} euro={this.state.euro} exchangeRate={this.state.exchangeRate} />
</div>
)
}
I am doing my first project using React and there is one thing I can't figure out. So I have many different Type components which are being set as the main component's TypesPage state. And when the onChange event happens on Type component I want to know which type it is in a TypesPage state or what index it is in a types array, so I can reupdate my data state.
Inside handleChange function I used jQuery's grep function comparing clicked Type title value with all the types array, but I am sure that is not the right way to do it and it would be an overkill with huge arrays.
Why I want to know which
handleChange:function(element, event){
var typeIndex;
$.grep(types, function(e, index){
if(element.title === e.title){
typeIndex = index
}
});
types[typeIndex] //Now I know that this is the Type that was changed
}
Fiddle
var types = [
{
type_id: 1,
type_name: "Logo"
},
{
type_id: 2,
type_name: "Ad"
},
{
type_id: 3,
type_name: "Catalog"
},
];
var Type = React.createClass({
render: function() {
return(
<li>
<input type="text" value={this.props.title}
onChange={this.props.handleChange.bind(null, this.props)} />
</li>
);
}
});
var TypesContainer = React.createClass({
render: function() {
var that = this;
return(
<ul>
{this.props.data.map(function(entry){
return(
<Type
key={entry.type_id}
title={entry.type_name}
handleChange={that.props.handleChange}
/>
);
})}
</ul>
);
}
});
var TypesPage = React.createClass({
getInitialState: function(){
return({data: types})
},
handleChange: function(element, event){
},
render: function() {
return(
<TypesContainer
data={this.state.data}
handleChange={this.handleChange}
/>
);
}
});
ReactDOM.render(
<TypesPage />,
document.getElementById('container')
);
I prefer ES6. The problem is, you have to bind your handleChange event with correct context of this and pass your arguments which you are expect to get inside your handle. See example below
class Example extends React.Component {
constructor(){
super();
this.state = {
data: [{id: 1, type: 'Hello'},{id: 2, type: 'World'},{id: 3, type: 'it"s me'}],
focusOn: null
};
}
change(index,e){
const oldData = this.state.data;
oldData[index].type = e.target.value;
this.setState({data:oldData, focusOn: index})
}
render(){
const list = this.state.data.map((item,index) =>
// this is the way how to get focused element
<input key={item.id} value={item.type} onChange={this.change.bind(this, index)}/>
);
return <div>
{list}
<p>Focused Element with index: {this.state.focusOn}</p>
</div>
}
}
React.render(<Example />, document.getElementById('container'));
fiddle
Thanks
I have created the form validation with a structure like this
var Signin = React.createClass({
render: function() {
return (
<Form>
<Input type="text" name="email" labelName="Email" rules="isEmail" error:"Email not valid" />
<Input type="password" name="password" labelName="Password" rules="isLength:6" error:"Passowrd not valid"/>
</Form>
);
}
});
because, for example, the "Email" input will be used in different part of application, I would avoid to add the same attributes (name, type, labelName, rules and error) every time. So I would create something like this
var InputEmail = React.createClass({
render: function () {
return (
<Input type="text" name="email" labelName="Email" rules="isEmail" error="Email not valid"/>
)
}
});
var InputPassword = React.createClass({
render: function () {
return (
<Input type="password" name="password" labelName="Password" rules="isLength:6" error="Passwordnot valid"/>
)
}
});
So the Signin Component should be
var Signin = React.createClass({
render: function() {
return (
<Form>
<InputEmail />
<InputPassword />
</Form>
);
}
});
but in this way, I get two errors:
I can't find anymore in the Form the props.name of Input because
there isn't in InputEmail;
in the render function of Input the state is null
How could I create a reausable/inherits components? I failed using both the composition pattern and the mixins
I added my full code: Form
var Form = React.createClass({
getInitialState: function () {
return {
isValid : false,
isSubmitting: false
}
},
componentWillMount: function(){
this.model = {};
this.inputs = {};
this.registerInputs(this.props.children);
},
registerInputs: function(children){
React.Children.forEach(children, function (child) {
if (child.props.name) {
child.props.attachToForm = this.attachToForm;
child.props.detachFromForm = this.detachFromForm;
child.props.validate = this.validate;
}
if (child.props.children) {
this.registerInputs(child.props.children);
}
}.bind(this));
},
attachToForm: function (component) {
this.inputs[component.props.name] = component;
this.model[component.props.name] = component.state.value;
this.validate(component);
},
detachFromForm: function (component) {
delete this.inputs[component.props.name];
delete this.model[component.props.name];
},
validate: function (component) {
var isValid = true;
// validation code
component.setState({
isValid: isValid,
}, this.validateForm);
},
validateForm: function () {
var formIsValid = true;
var inputs = this.inputs;
Object.keys(inputs).forEach(function (name) {
if (!inputs[name].state.isValid) {
formIsValid = false;
}
});
this.setState({
isValid: formIsValid
});
},
updateModel: function (component) {
Object.keys(this.inputs).forEach(function (name) {
this.model[name] = this.inputs[name].state.value;
}.bind(this));
},
submit: function (event) {
event.preventDefault();
this.setState({
isSubmitting : true
});
this.updateModel();
console.log(this.model);
},
render: function () {
return (
<form className="ui form" onSubmit={this.submit}>
{this.props.children}
<button className="ui button" type="submit" disabled={this.state.isSubmitting}>Accedi</button>
</form>
);
}
});
Input
var Input = React.createClass({
getInitialState: function(){
return {
value : this.props.value || "",
isValid: true
}
},
setValue: function (event) {
this.setState({
value: event.target.value
}, function () {
this.props.validate(this);
}.bind(this));
},
componentWillMount: function () {
if (this.props.required) {
this.props.validations = this.props.validations ? this.props.validations + ',' : '';
this.props.validations += 'isLength:1';
}
// ERROR: TypeError: this.props.attachToForm is not a function
this.props.attachToForm(this);
},
componentWillUnmount: function () {
this.props.detachFromForm(this);
},
render: function () {
var className = "field";
if(this.props.className){
className += " " + this.props.className;
}
if(this.props.required){
className += " required";
}
var Label;
if(this.props.labelName){
Label = (<label htmlFor={this.props.name}>{this.props.labelName}</label>);
}
var Error;
if(!this.state.isValid){
Error = (<div className="ui">{this.props.error || this.props.name + " not valid"}</div>);
};
return (
<div className={className}>
{Label}
<input type={this.props.type || "text"} id={this.props.name} name={this.props.name} onChange={this.setValue} value={this.state.value} />
{Error}
</div>
);
}
});
With this works
ReactDOM.render(
<Form>
<Input type="text" name="email" labelName="Email" rules="isEmail" error:"Email not valid"/>
</Form>,
document.getElementById('app')
);
In this way I get:
"TypeError: this.props.attachToForm is not a function
this.props.attachToForm(this);"
ReactDOM.render(
<Form>
<InputEmail/>
</Form>,
document.getElementById('app')
);
P.S: I tried to add this code on jsfiddle but I get "!TypeError: can't define property "attachToForm": Object is not extensible"
jsfiddle
There are 2 main issues with your setup:
Your <Form> is set up in such a way, that the children of the form need to have props, otherwise it does not work.
The <InputEmail> wrapper is incomplete. It needs to pass along all props to the <Input>, including the Form functions passed down.
Ad 1: Fix the form, to ensure validation methods are added
The reason you get the error is because the children of your <Form> need to have props.name. It then registers the functions of the form (including attachToForm), by adding them to the children. This is done in the method registerInputs().
In the original variant, the <Input> component has props, so all goes well.
In the adapted variant, the wrapper <InputEmail> no longer has props, so the attachToForm() and other functions are not added to props, and you get the error when the <Input> tries to invoke the function.
Simplest way to fix this: add at least 1 prop in the render function, and check this in the registerInputs(), e.g.:
ReactDOM.render(
<Form>
<InputEmail initialValue={'name#domain.com'}/>
</Form>,
document.getElementById('app')
);
And in registerInputs(), change the line:
if (child.props.name) {
to:
if (child.props.initialValue) {
2. Extend <InputEmail> wrapper to pass down functions as well
Simplest way to do this is to add {...this.props}, like this:
var InputEmail = React.createClass({
render: function () {
return (
<Input {...this.props}
type="text" name="email" labelName="Email" rules="isEmail" error="Email not valid"/>
)
}
});
That way, the functions passed down by <Form> to the <InputEmail> component (as well as any other props), will be passed down to the <Input> component.
PS: The code inside registerInputs() that checks for child.props.children does not work as intended: at the time it is invoked, the <InputEmail> component does not have children. Like the name implies, it checks for children passed down as props. And the only prop passed was initialValue.
As minor issues I would suggest to make 2 more changes:
In registerInputs(), you directly modify props. This is generally not a good idea. Better is to make a copy of props, and add your form-methods to the copy. You can use React.Children.map to do this. See official docs here.
Instead of hard-coding the name="email" etc of your <Input> component, inside <InputEmail>, better is to put the default values of these in default values of props, using propTypes, as explained here in official docs.
I'm spending time on something probably simple:
I'd like to implement a search bar, ideally updating the list of item as-you-type. My small app uses React and Backbone (for models and collections).
Displaying the list isn't too hard, it all works perfectly doing this (the mixin i'm using basically allows easy collections retrieval):
var List = React.createClass ({
mixins: [Backbone.React.Component.mixin],
searchFilter: function () {
//some filtering code here, not sure how (filter method is only for arrays...)
}
}
getInitialState: function () {
initialState = this.getCollection().map(function(model) {
return {
id: model.cid,
name: model.get('name'),
description: model.get('description')
}
});
return {
init: initialState,
items : []
}
},
componentWillMount: function () {
this.setState({items: this.state.init})
},
render: function(){
var list = this.state.items.map(function(obj){
return (
<div key={obj.id}>
<h2>{obj.name}</h2>
<p>{obj.description}</p>
</div>
)
});
return (
<div className='list'>
{list}
</div>
)
}
});
Now i've tried with no success to first translate the backbone collection into "state" with the getInitialState method, my idea was to proxy through a copy of the collection, which then could hold the search results. I'm not showing here my attemps for the sake of clarity(edit: yes i am), could someone guide me to the right approach? Thanks in advance.
There are many ways to accomplish this, but the simplest (in my opinion) is to store your search criteria in the List component's state and use it to filter which items from your collection get displayed. You can use a Backbone collection's built in filter method to do this.
var List = React.createClass ({
mixins: [Backbone.React.Component.mixin],
getInitialState: function () {
return {
nameFilter: ''
};
},
updateSearch: function (event) {
this.setState({
nameFilter: event.target.value
});
},
filterItems: function (item) {
// if we have no filter, pass through
if (!this.state.nameFilter) return true;
return item.name.toLowerCase().indexOf(this.state.nameFilter) > -1;
},
render: function(){
var list = this.props.collection
.filter(this.filterItems.bind(this))
.map(function(obj){
return (
<div key={obj.id}>
<h2>{obj.name}</h2>
</div>
)
});
return (
<div className='list'>
{list}
<input onChange={this.updateSearch} type="text" value={this.state.nameFilter}/>
</div>
)
}
});
var collection = new Backbone.Collection([
{
name: 'Bob'
},
{
name: 'Bill'
},
{
name: 'James'
}
]);
React.render(<List collection={collection}/>, document.body);
jsbin
The search criteria could easily be passed down from a parent component as a prop, so the search input does not have to live inside your List component.
Eventually I also found a different solution (below), but it involves copying the entire collection into state, which is probably not such a good idea...
var List = React.createClass ({
mixins: [Backbone.React.Component.mixin],
searchFilter: function () {
var updatedlist = this.state.init;
var searchText = this.refs.searchbar.getDOMNode().value
updatedlist = updatedlist.filter(function (item) {
return item.name.toLowerCase().search(
searchText.toLowerCase()) !== -1
});
this.setState({items: updatedlist})
}
},
getInitialState: function () {
initialState = this.getCollection().map(function(model) {
return {
id: model.cid,
name: model.get('name'),
description: model.get('description')
}
});
return {
init: initialState,
items : []
}
},
componentWillMount: function () {
this.setState({items: this.state.init})
},
render: function(){
var list = this.state.items.map(function(obj){
return (
<div key={obj.id}>
<h2>{obj.name}</h2>
<p>{obj.description}</p>
</div>
)
});
return (
<div className='list'>
<input ref='searchbar' type="text" placeholder="Search" onChange={this.searchFilter}/>
{list}
</div>
)
}
});