未加星标

Autocomplete Widget with React

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二05 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

This project will guide you through building an autocomplete function similar to the one that you might see in Slack (a popular messaging app), as shown in figure 1, when you type something in the search box. For simplicity, our widget will work with room names (the rooms in a chat application).


Autocomplete Widget with React
Figure 1

The autocomplete widget will have (figure 2):

An input field A list of options filtered according to the entered characters An Add button (figure 3)
Autocomplete Widget with React
Figure 2

The filtering of the matches will be done using the entered characters as the first characters of the option. In other words, there is a simple comparison that allows us to autocomplete the name of the room (figure X). For example, if you type “mac” and you have “Mac OS X” and “Apple Mac,” then only “Mac OS X” will be shown as a match, not both options.

[Sidenote]

Reading blog posts is good, but watching video courses is even better because they are more engaging.

A lot of developers complained that there is a lack of affordable quality video material on Node. It's distracting to watch to YouTube videos and insane to pay $500 for a Node video course!

Go check out Node University which has FREE videos courses on Node: node.university .

[End of sidenote]

NOTE : The options will be stored in MongoDB using the native MongoDB Node.js driver. For the web server, I’m using Express.js.

The Add button will be shown only when there are no matches (figure 3).

Sidenote: If you like this post and interested in a corporate on-site javascript, Node.js and React.js training to boost productivity of your team, then contact NodeProgram.com .


Autocomplete Widget with React
Figure 3

The new option will be saved to the database via an XHR call to our REST API. We can use this new room name in the future (figure 4), just like our initial room names.


Autocomplete Widget with React
Figure 4

To get started with the project, create a new folder. The project structure looks like this:

/autocomplete
/__tests__
autocomplete.test.js
/node_modules
/public
/css
bootstrap.css
/js
app.js
/src
/build
app.js
autocomplete.js
app.jsx
autocomplete.jsx
/views
index.handlebars
gulpfile.js
index.js
package.json
README.md
rooms.json

Let’s cover them one by one. __tests__ is the folder with the Jest tests. By now familiar to you, the node_modules folder is the Node.js dependencies folder (from npm’s package.json ). Then there are public, public/css , and public/js folders, which contain the static files for our application. The app.js file will be bundled by Gulp and Browserify from the dependencies and the JSX source code. The source code itself is in the src folder. I created src/build for the files compiled from JSX into native JavaScript.

views is just a folder for Handlebars templates. If you feel confident about your React skills by now, you don’t have to use a template engine; you can use React as the Node.js template engine!

In the root of the project, you will find gulpfile.js , which enables build tasks, package.json , which contains project metadata, rooms.json , which contains the MongoDB seed data, and index.js , with the Express.js server and its routes for the API server (GET and POST /rooms ).

This project’s structure is somewhat similar to ch8/board-react2 , and we’ll be rendering React components on the server, testing them with Jest, and making AJAX/XHR requests with request within the Reflux data store.

Start by copying (it’s better to copy by typing instead of copying and pasting) this package.json :

{
"name": "autocomplete",
"version": "1.0.0",
"description": "React.js autocomplete component with Express.js and MongoDB example.",
"main": "index.js",
"scripts": {
"test": "jest",
"start": "gulp",
"seed": "mongoimport rooms.json --jsonArray --collection=rooms --db=autocomplete"
},
"keywords": [
"react.js",
"express.js",
"mongodb"
],
"author": "Azat Mardan",
"license": "MIT",
"dependencies": {
"body-parser": "^1.13.2",
"compression": "^1.5.1",
"errorhandler": "^1.4.1",
"express": "^4.13.1",
"express-handlebars": "^2.0.1",
"express-validator": "^2.13.0",
"mongodb": "^2.0.36",
"morgan": "^1.6.1",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"reflux": "^0.3.0",
"request": "^2.65.0"
},
"devDependencies": {
"jest-cli": "0.5.10",
"react-addons-test-utils": "0.14.0",
"gulp": "^3.9.0",
"gulp-browserify": "^0.5.1",
"gulp-develop-server": "^0.4.3",
"gulp-nodemon": "^2.0.3",
"gulp-react": "^3.0.1",
"gulp-watch": "^4.2.4"
}
}

The interesting thing here is scripts :

"scripts": {
"test": "jest",
"start": "gulp",
"seed": "mongoimport rooms.json --jsonArray --collection=rooms --db=autocomplete"
},

The test is for running Jest tests, and start is for building and launching our server.

I also added seed data for the room names, which you can run with $ npm run seed .

The database name is autocomplete , and the collection name is rooms . This is the content of the rooms.json file:

[ {"name": "react"},
{"name": "node"},
{"name": "angular"},
{"name": "backbone"}]

Once you run the seed command, it will print this (MongoDB must be running as a separate process):

2015-10-19T15:26:39.632-0700 connected to: localhost
2015-10-19T15:26:39.793-0700 imported 4 documents

In this project, we’ll be using npm modules for the dependencies like React, request , and React DOM. This is possible due to Browserify. Here’s the gulpfile.js file, which has a scripts task in which Browserify parses src/build/app.js and includes all the dependencies in public/js/app.js :

var gulp = require('gulp'),
react = require('gulp-react'),
watch = require('gulp-watch'),
nodemon = require('gulp-nodemon'),
browserify = require('gulp-browserify')
gulp.task('build', function (done) {
return gulp.src('./src/*.jsx')
.pipe(react())
.pipe(gulp.dest('src/build'))
})
gulp.task('scripts', ['build'], function() {
gulp.src('./src/build/app.js')
.pipe(browserify({
insertGlobals : true,
debug: true
}))
.pipe(gulp.dest('./public/js'))
})
gulp.task('watch', ['build', 'scripts'], function(done){
gulp.watch('src/*.jsx', ['build','scripts'] )
})
gulp.task('nodemon', ['build', 'scripts'], function(done){
nodemon({ script: 'index.js',
ext: 'html js' })
})
gulp.task('default', ['build', 'scripts', 'watch', 'nodemon'])

For app.js to exist in the src/build folder, there is a build task that precedes the scripts task. The scripts task compiles all the JSX files into native JS files.

Lastly in gulpfile.js , the nodemon and watch tasks are for convenience. They’ll restart the Node.js server on a file change and rebuild JS files on a JSX file change, respectively.

An important part of the index.js file is the way we include the libraries and components:

ReactDOM = require('react-dom'),
ReactDOMServer = require('react-dom/server'),
React = require('react'),
Autocomplete = React.createFactory(require('./src/build/autocomplete.js'))

The index.js file has GET and POST routes for /rooms . If you’re not familiar with Express.js, there’s a quick-start guide in the cheatsheet . This post is on React, not Express. :) We’ll only cover the / route in Express. In it, we render React on the server by hydrating components with the room objects:

app.get('/', function(req, res, next) {
var url = 'http://localhost:3000/rooms'
req.rooms.find({}, {sort: {_id: -1}}).toArray(function(err, rooms){
if (err) return next(err)
res.render('index', {
autocomplete: ReactDOMServer.renderToString(Autocomplete({
options: rooms,
url: url
})),
props: '<script type="text/javascript">var rooms = ' + JSON.stringify(rooms) + ', url = "' + url + '"</script>'
})
})
})

This is the same approach that you saw in the previous example, ch8/board-react2 : the props are in the scripts tag.

There are two props to the Autocomplete component: options and url . The options are the names of the rooms for the chat and the url is the URL of the API server ( http://localhost:3000/rooms in our case).

According to the principles of TDD/BDD, let’s start with tests. In the __tests__/autocomplete.test.js file we have:

jest.autoMockOff()

The rooms variable is just hardcoded data for the room names:

var rooms = [
{ "_id" : "5622eb1f105807ceb6ad868b", "name" : "node" },
{ "_id" : "5622eb1f105807ceb6ad868c", "name" : "react" },
{ "_id" : "5622eb1f105807ceb6ad868d", "name" : "backbone" },
{ "_id" : "5622eb1f105807ceb6ad868e", "name" : "angular" }
]

Then we include the libraries. They are npm modules, except for src/build/autocomplete.js , which is a file:

var TestUtils = require('react-addons-test-utils'),
React = require('react'),
ReactDOM = require('react-dom'),
Autocomplete = require('../src/build/autocomplete.js'),

The fD object is just a convenience (less typing means fewer errors):

fD = ReactDOM.findDOMNode

The next line is using TestUtils from react-addons-test-utils to render the Autocomplete component:

var autocomplete = TestUtils.renderIntoDocument(
React.createElement(Autocomplete, {
options: rooms,
url: 'test'
})
)

Now we get the input field, which will have a class option-name . These will be our options:

var optionName = TestUtils.findRenderedDOMComponentWithClass(autocomplete, 'option-name')

Then, we can write the actual tests:

describe('Autocomplete', function() {

We can get all the option-name elements from the widgets and compare them against the number 4, which is the number of rooms in the rooms array:

it('have four initial options', function(){
var options = TestUtils.scryRenderedDOMComponentsWithClass(autocomplete, 'option-list-item')
expect(options.length).toBe(4)
})

The next test changes the input field value and then checks for that value and the number of the offered autocomplete option. There should be only a single match, which is react :

it('change options based on the input', function(){
expect(fD(optionName).value).toBe('')
fD(optionName).value = 'r'
TestUtils.Simulate.change(fD(optionName))
expect(fD(optionName).value).toBe('r')
options = TestUtils.scryRenderedDOMComponentsWithClass(autocomplete, 'option-list-item')
expect(options.length).toBe(1)
expect(fD(options[0]).textContent).toBe('#react')
})

The last test will change the room name field to ember , and there should be no matches, only the Add button:

it('offer to save option when there are no matches', function(){
fD(optionName).value = 'ember'
TestUtils.Simulate.change(fD(optionName))
options = TestUtils.scryRenderedDOMComponentsWithClass(autocomplete, 'option-list-item')
expect(options.length).toBe(0)
var optionAdd = TestUtils.findRenderedDOMComponentWithClass(autocomplete, 'option-add')
expect(fD(optionAdd).textContent).toBe('Add #ember')
})
})

The tests should fail for now, but that’s okay. Onward to implementing our browser script, which is src/app.jsx :

React = require('react')
ReactDOM = require('react-dom')
request = require('request')
Autocomplete = require('./autocomplete.js')
ReactDOM.render(<Autocomplete options={rooms} url={url}/>, document.getElementById('autocomplete'))

The global vars rooms and url will be provided via the props local from the Express.js tag (the script HTML tag). In the index.handlebars file, you can see the props and autocomplete locals being output:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Autocomplete with React.js</title>
<meta name="description" content="" />
<meta name="author" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link type="text/css" rel="stylesheet" href="/css/bootstrap.css" />
</head>
<body>
<div class="container-fluid">
<div>{{{props}}}</div>
<div class="row-fluid">
<div class="span12">
<div id="content">
<div class="row-fluid" id="autocomplete" />{{{autocomplete}}}</div>
</div>
</div>
</div>
<script type="text/javascript" src="/js/app.js"></script>
</body>
</html>

Finally, the autocomplete.jsx file with our component. We start by importing the libraries in the CommonJS/Node.js style (thanks to Browserify, this will be bundled for the browser’s consumption):

var React = require('react'),
ReactDOM = require('react-dom'),
request = require('request'),
Reflux = require('reflux')

Again, this alias is for convenience:

var fD = ReactDOM.findDOMNode

We’ll be using Reflux. These are the actions for our data store:

var Actions = Reflux.createActions([
'loadOptions',
'addOption',
'setUrl',
'setOptions'
])

Let’s create the store and set up the actions. We can use the listenables property:

var optionsStore = Reflux.createStore({
listenables: [Actions],

onSetUrl will set the REST API server URL to perform AJAX/XHR requests:

onSetUrl: function(url){
this.url = url
},

onSetOptions will create a property called options . This will be all the available options (i.e., unfiltered):

onSetOptions: function(options){
this.options = options
},

In onLoadOptions , we perform the GET request using the request library. It’s similar to jQuery’s $.get :

onLoadOptions: function(options) {
this.options = options
request({url: this.url},function(error, response, body) {
if(error || !body){
return console.error('Failed to load')
}
body = JSON.parse(body)

Once we get the options, we assign them to this.options and trigger the callback, which is in the component that listens to the loadOptions event:

this.options = body
this.trigger(body)
}.bind(this))
},

The onAddOptions method performs a POST request and puts the newly created record into the this.options array:

onAddOptions: function(option, callback){
request({url: this.url, method: 'POST', json: {name: option}}, function(error, response, body) {
if(error || !body){
return console.error('Failed to save')
}
this.options.unshift(body)
callback(body)
this.trigger(this.options)
}.bind(this))
}
})

We’re using CommonJS syntax, so we can declare the Autocomplete component and export it like this:

module.exports = React.createClass({

The next line enables the auto-syncing of the optionsStore ’s options with our state options:

mixins: [Reflux.connect(optionsStore,'options')],

In the initial state function, we set the URL and options from props. The filtered options will be the same as all of the options, and the current option (input field value) is empty:

getInitialState: function(){
Actions.setUrl(this.props.url)
Actions.setOptions(this.props.options)
return {options: this.props.options,
filteredOptions: this.props.options,
currentOption: ''
}
},

When the component is about to be mounted, we load the options from the server by invoking the optionsStore action:

componentWillMount: function(){
Actions.loadOptions(this.props.options)
},

The filter method will be called on every change of the <input> field. The goal is to leave only the options that match user input:

filter: function(e){
this.setState({
currentOption: e.target.value,
filteredOptions: (this.state.options.filter(function(option, index, list){
return (e.target.value === option.name.substr(0, e.target.value.length))
}))
}, function(){
})
},

As for addOption , this method handles the addition of a new option (in the event that there are no matches) by invoking the store’s action:

addOption: function(e){
var currentOption = this.state.currentOption
Actions.addOption(this.state.currentOption, function(){

There is a callback in the action that will ensure that the list of options is updated once the new value is part of the list:

this.filter({target: {value: currentOption}})
}.bind(this))
},

Finally, the render method has a controlled component, <input> , with an onChange event listener, this.filter :

render: function(){
return (
<div className="form-group">
<input type="text" className="form-control option-name" onChange={this.filter} value={this.currentOption} placeholder="React.js"></input>

The list of filtered options is powered by the state filteredOptions , which is updated in the filter method. We simply iterate over it and print _id as keys and links with option.name :

{this.state.filteredOptions.map(function(option, index, list){
return <div key={option._id}><a className="btn btn-default option-list-item" href={'/#/'+option.name} target="_blank">#{option.name}</a></div>
})}

The last element is the Add button, which is shown only when there is no filteredOptions (no matches):

{function(){if (this.state.filteredOptions.length == 0 && this.state.currentOption!='')
return <a className="btn btn-info option-add" onClick={this.addOption}>Add #{this.state.currentOption}</a>
}.bind(this)()}
</div>
)
}
})

If you’ve followed all the steps, you should be able to install the dependencies with

$ npm install

and then launch the app with this command (you must have started MongoDB first with $ mongod ):

$ npm start

The tests will pass after you run the command:

$ npm test

Optionally, you can seed the database with $ npm run seed .

If for some reason your project is not working, the full tested source code is in ch8/autocomplete and on GitHub .

--

Azat Mardan


Autocomplete Widget with React

https://www.linkedin.com/in/azatm

To contact Azat, the main author of this blog, submitthe contact form.

Also, make sure to get 3 amazing resources to FREE when you sign up for the newsletter.

Simple.

Easy.

No commitment.

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

主题: ReactNode.jsMongoDBJavaJavaScriptRESTGitGulpjQueryGitHub
分页:12
转载请注明
本文标题:Autocomplete Widget with React
本站链接:http://www.codesec.net/view/483363.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(38)