I decided to make a modular searchbar for my website in React. React is really awesome because it bundles up frontend code into modular almost functional components that are very simple to reason about. If you are new to React this might not be very easy to read through so I highly suggest looking at facebook's React tutorial. That said, have a look below to see how to create an awesome searchbar in under 200 lines of code.
So I usually think about building my React apps in a top-down approach. This is roughly what I envisioned when I first started building the searchbar.
<searchbox>
<searchbar> </searchbar>
<postlist>
<post></post>
<post></post>
.............
<post></post>
</postlist>
</searchbox>
Notice I did not use divs in this mockup, I abstracted away the HTML and am thinking in React elements I am going to create. I think this is extremely powerful and lets us work at a higher level of abstraction and is something I personally like a lot about React.
I then started thinking about the dataflow of the program. Specifically, we expect data to come in from the top of the pipeline, where it is filtered based on the text in the searchbar and then passed down to the postlist which populates a list of filtered postdata. Thus it makes sense to have the ownership of the data and query text to be the searchbox.
Given this information I was able to create the backbone of the React app below.
React.render(
<SearchBox data={blog_data}/>,
document.getElementById('search-container')
);
var SearchBox = React.createClass({
getInitialState:function(){
return{
query_text: '',
data : [],
filtered_data: []
}
},
render: function(){
return(
<div id="search-box">
<SearchBar query={this.state.query} />
<PostList data={this.state.filtered_data} query={this.state.query} />
</div>
);
}
});
var SearchBar = React.createClass({
render:function(){
return <input type="text"
placeholder="Search for a post, title, or keyword"
required="required"
value={this.props.query}
}
});
var PostList = React.createClass({
render: function(){
var posts = this.props.data.map(function(post){
return (
<Post title={post.title} url={post.url} img={post.image} keywords={post.keywords} />
);
});
return(
<ul id="search-items">
{posts}
</ul>
);
}
});
var Post = React.createClass({
render: function() {
return (
<li>
<a href= {this.props.url}>
<div className = "row">
<div className="large-3 columns">
<img src={this.props.img}/>
</div>
<div className="large-9 columns">
{this.props.title}
</div>
</div>
</a>
</li>
);
}
});
Since we have the barebones of the searchbar the next step is to define how the interaction will work. We know a user will type in the searchbar and we want to modify the query text which is located in the searchbox which is in fact ABOVE the searchbar on the dataflow pipeline.
Because React really only has one-way data flow downstream this at first appears to be a challenge. One way to get around this is instead passing a function down as a prop which allows modification of the upstream element using the function.
var Post = React.createClass({
render: function() {
return (
<li className={this.props.is_selected}>
<a href= {this.props.url}>
<div className = "row">
<div className="large-3 columns">
<img src={this.props.img}/>
</div>
<div className="large-9 columns">
{this.props.title}
</div>
</div>
</a>
</li>
);
}
});
var PostList = React.createClass({
render: function(){
var posts = this.props.data.map(function(post){
return (
<Post title={post.title} url={post.url} img={post.image} keywords={post.keywords} is_selected={is_selected}/>
);
});
return(
<ul id="search-items">
{posts}
</ul>
);
}
});
var SearchBar = React.createClass({
update_search:function(){
var query_text=this.refs.search_input.getDOMNode().value;
this.props.update_searchbox(query_text);
},
render:function(){
return <input type="text"
id="search-bar"
ref="search_input"
placeholder="Search for a post, title, or keyword"
required="required"
value={this.props.query}
onChange={this.update_search}/>
}
});
var SearchBox = React.createClass({
getInitialState:function(){
return{
query_text: '',
data : [],
filtered_data: []
}
},
get_filt_data: function(query_text, numdata){
//data processing goes here
return filtered_data;
},
set_filt_data: function(filt_data){
this.setState({filtered_data: filt_data})
},
set_query_text: function(q_text){
this.setState({query_text: q_text})
},
/*
Due to the one-way data flow of React, we need to a way to modify the searchbox and update its state from "downstream".
To do this we pass a function as a prop that lets us modify the state of the searchbox object.
*/
update_state: function(query_text){
this.set_query_text(query_text);
this.set_filt_data(
this.get_filt_data(query_text, 5)
);
},
render: function(){
return(
<div id="search-box">
<SearchBar update_searchbox={this.update_state} query={this.state.query} />
<PostList data={this.state.filtered_data} query={this.state.query} />
</div>
);
}
});
React.render(
<SearchBox data={blog_data}/>,
document.getElementById('search-container')
);
The last step of the process is filling in the black box of filtering the data to obtain only the search results that match the query text.
The search algorithm is an approximation of fuzzy search.
- split the query text into query words
- look through every post and filter out data that does not meet the minimum criteria for matching the query words
- the min criteria for post matching is that every query word is represented in some place in the keywords of the post
- keywords of the post are abstracted away; needless to say there is some endpoint api that transforms a post text and its metadata into keywords which define what it talks about. The algorithm I used may be expanded in a separate post.
This is much easier to understand with an example:
querytext= "program"
data=[
{"title": "post1",
"keywords": ["programming", "python"]},
{"title": "post2",
"keywords": ["programming", "ruby"]},
{"title": "post3",
"keywords": ["on", "off"]}
]
filtered_data =[
{"title": "post1",
"keywords": ["programming", "python"]},
{"title": "post2",
"keywords": ["programming", "ruby"]}
]
``` querytext= "program rub"
filtered_data=[
{"title": "post2",
"keywords": ["programming", "ruby"]},
]
<hr>
querytext= "on"
filtered_data =[ {"title": "post1", "keywords": ["programming", "python"]}, {"title": "post3", "keywords": ["on", "off"]} ] ```
get_filt_data: function(query_text, numdata){
lower_text = query_text.toLowerCase();
query_words = lower_text.split(' ').filter(function(word){
return word !='' && word !=' ';
});
var filt_data = this.props.data.filter(function(post){
var boolwords = query_words.map(function(word){
return _.some(
post.keywords.map(function(kword){
return kword.indexOf(word)!=-1
})
);
});
return (
_.all(boolwords)
)
});
var top_filt_data = _.first(filt_data, numdata)
return top_filt_data;
}
And that is it! You should have a fully functioning searchbar in React.
To see a more fully functioning searchbar with extra features such as navigation using keystrokes, styling, and more, please see the source of the searchbar on my github here.
var Post = React.createClass({
render: function() {
return (
<li className={this.props.is_selected}>
<a href= {this.props.url}>
<div className = "row">
<div className="large-3 columns">
<img src={this.props.img}/>
</div>
<div className="large-9 columns">
{this.props.title}
</div>
</div>
</a>
</li>
);
}
});
var PostList = React.createClass({
render: function(){
var posts = this.props.data.map(function(post){
return (
<Post title={post.title} url={post.url} img={post.image} keywords={post.keywords} is_selected={is_selected}/>
);
});
return(
<ul id="search-items">
{posts}
</ul>
);
}
});
var SearchBar = React.createClass({
update_search:function(){
var query_text=this.refs.search_input.getDOMNode().value;
this.props.update_searchbox(query_text);
},
render:function(){
return <input type="text"
id="search-bar"
ref="search_input"
placeholder="Search for a post, title, or keyword"
required="required"
value={this.props.query}
onChange={this.update_search}/>
}
});
var SearchBox = React.createClass({
getInitialState:function(){
return{
query_text: '',
data : [],
filtered_data: []
}
},
get_filt_data: function(query_text, numdata){
lower_text = query_text.toLowerCase();
query_words = lower_text.split(' ').filter(function(word){
return word !='' && word !=' ';
});
var filt_data = this.props.data.filter(function(post){
var boolwords = query_words.map(function(word){
return _.some(
post.keywords.map(function(kword){
return kword.indexOf(word)!=-1
})
);
});
return (
_.all(boolwords)
)
});
var top_filt_data = _.first(filt_data, numdata)
return top_filt_data;
},
set_filt_data: function(filt_data){
this.setState({filtered_data: filt_data})
},
set_query_text: function(q_text){
this.setState({query_text: q_text})
},
/*
Due to the one-way data flow of React, we need to a way to modify the searchbox and update its state from "downstream".
To do this we pass a function as a prop that lets us modify the state of the searchbox object.
*/
update_state: function(query_text){
this.set_query_text(query_text);
this.set_filt_data(
this.get_filt_data(query_text, 5)
);
},
render: function(){
return(
<div id="search-box">
<SearchBar update_searchbox={this.update_state} query={this.state.query} />
<PostList data={this.state.filtered_data} query={this.state.query} />
</div>
);
}
React.render(
<SearchBox data={blog_data}/>,
document.getElementById('search-container')
);
});