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>
)
}
});
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 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 am very new to React and am just getting my feet wet. I'm having a hard time understand why this isn't re-rending the List. Here is my code:
app.jsx
var Hello = React.createClass({
getInitialState: function() {
return {
links: ['test ']
}
},
render: function() {
return <div className = "row">
<Submission linkStore = {this.state.links}/>
<List links = {this.state.links} />
</div>
}
});
var element = React.createElement(Hello, {});
ReactDOM.render(element, document.querySelector('.container'));
In my submission.jsx I have this function to push info into the links array
handleSubmitClick: function() {
this.props.linkStore.push(this.props.text)
this.setState({text: ''})
console.log(this.props.linkStore)
}
My list.jsx looks like this
module.exports = React.createClass({
getInitialState: function() {
return {
links: this.props.links
}
},
render: function() {
return <div>
{this.props.links}
</div>
}
});
Everything works as intended and I can get the test to show appropriately.
I am aware that this isn't going to show up as an actual list and that I should create a list component to show the items in list form. I'm just trying to run tests along the way to see how everything works.
Use parent state instead of child props.
try this
app.jsx
var Hello = React.createClass({
getInitialState: function() {
return {
links: ['test ']
}
},
handleListSubmitClick: function(params) {
this.setState({links:params});
},
render: function() {
return <div className = "row">
<Submission linkStore = {this.state.links} handleListSubmitClick={this.handleListSubmitClick}/>
<List links = {this.state.links} />
</div>
}
});
submission.jsx
handleSubmitClick: function() {
var linkStore = this.props.linkStore;
linkStore.push(this.props.text)
this.setState({text: ''})
this.props.handleListSubmitClick(linkStore);
}
but I don't understand this.props.text. input's value using this.refs.ref
list.jsx
module.exports = React.createClass({
getInitialState: function() {
return {
links: this.props.links
}
},
render: function() {
return <div>
{this.props.links}
</div>
}
});
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 :)
I'm starting out looking at react, I've seen a good example here https://scotch.io/tutorials/learning-react-getting-started-and-concepts
however I'm not sure where 'item' is coming from, it doesn't appear to be declared anywhere. I've highlighted item in the code below.
/** #jsx React.DOM */
var List = React.createClass({
render: function(){
return (
<ul>
{
this.props.items.map(function(**item**) {
return <li key=**{item}>{item}**</li>
})
}
</ul>
)
}
});
var FilteredList = React.createClass({
filterList: function(event){
var updatedList = this.state.initialItems;
updatedList = updatedList.filter(function(**item**){
return **item**.toLowerCase().search(event.target.value.toLowerCase()) !== -1;
});
this.setState({items: updatedList});
},
getInitialState: function(){
return {
initialItems: [
"Apples",
"Broccoli",
"Chicken",
"Duck",
"Eggs",
"Fish",
"Granola",
"Hash Browns"
],
items: []
}
},
componentWillMount: function(){
this.setState({items: this.state.initialItems})
},
render: function(){
return (
<div className="filter-list">
<input type="text" placeholder="Search" onChange={this.filterList}/>
<List items={this.state.items}/>
</div>
);
}
});
React.renderComponent(<FilteredList/>, document.getElementById('mount-point'));
updatedList is an array.
Array.filter takes a function as an argument that will be passed in a variable. They are using item as that variable name.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter