Recently I had to create an autocomplete component for a React project, the difference here is that even though the data was coming from the popular NoSQL datastore MongoDB I had to pull it from several sources.
With Mongo and Node.JS API calls you only really have one hit you can use in order to request all the data you need for the request.
Now you can use Async with Mongoose to recall several tables at once:
Server: With Async
var team = require('../../models/team.js');
var user = require('../../models/user.js');
var async = require('async');
try {
async.parallel({
teams: function(cb){
team.find({}, '_id title')
.lean() // cut out virtuals
.exec(cb)
},
users: function(cb){
user.find({}, '_id email')
.lean() // cut out virtuals
.exec(cb)
}
}, function(err, results){
const teams = results.teams // store teams data
const users = results.users // store users data
// and then process the results
But for autocomplete it seems impractical to use, so we’ll be using Mongoose’s aggregate feature link and return just the data we need in the format we want.
Client: AutoComplete Component
First we’ll create a React component to autocomplete team names, we’ll use the isomorphic-fetch component to trigger the call. Using React-Select to render the results and using props to pass back and forth the selected data to the parent.
Here the autocomplete component returns all teams with their assigned user.
import React, { Component } from 'react';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';
class AutoCompleteTeams extends Component {
// define initial props
static defaultProps = {
className: '',
update: null,
initialValue: { _id: '', title: '' }
}
// define initial state
constructor(props) {
super(props);
this.state = {
selectedArray: new Object()
}
}
componentWillReceiveProps(newProps){
if (newProps === this.props) {
this.setState({
selectedArray: new Object()
}, () => {
return;
})
}
this.setState({
selectedArray: newProps.initialValue || new Object()
}, function() {
})
}
// onChange (item selected), return value to parent
// => { title: 'Alexandria (john@smith.com)' }
onChange = (value) => {
this.setState({
selectedArray: value,
}, function() {
this.props.update(value);
});
}
// on autocomplete, perform GET request.
// We'll also send credentials as we're operating
// within a secure session
autocomplete = (input) => {
if (!input) {
return Promise.resolve({ options: [] });
}
return fetch(`/v1/users/autocomplete/teams?q=${input}`, {
method: 'GET',
credentials: 'include' })
.then((response) => response.json())
.then((json) => {
return { options: json };
});
}
// render autocomplete using Select.Async displaying
render() {
const { className} = this.props
return (
<div className={`form-control ${className}`}>
<Select.Async
value={this.state.selectedArray}
onChange={this.onChange}
valueKey="_id" // define which value to use when item selected
labelKey="title" // define which field to display in select
loadOptions={this.autocomplete}
backspaceRemove={true}
/>
</div>
)
}
}
export default AutoCompleteTeams;
Client: Parent Usage
We can then add it to our parent component via:
import React, { Component } from 'react';
import AutoCompleteTeams from './../sharedComponents/AutoCompleteTeams'
class AddTeamLeader extends Component {
setTeam = (value) => {
this.setState({ user: value })
}
render() {
return (<div>
<AutoCompleteTeams
className="col-xs-12 col-sm-6 col-md-8"
name="title"
id="title"
initialValue={this.state.team}
update={this.setTeam}
/>
</div>)
}
}
Server: Aggregate Query
Now we’ll define the get request, using aggregation to return a list of teams with their respective leader’s email in brackets.
var team = require('../../models/team.js');
exports.autocompleteTeams = function(req, res) {
const q = req.query.q || ''
if (q) {
team.aggregate([
// Order is important, each extra match drills down the results available.
{$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
{$project: {"title" :
{ $concat : [
"$title",
" (",
{ $arrayElemAt:["$leader.email", 0] },
")"
] }
}},
{$match: {"title": { "$regex": q, "$options": "i" }}},
{$sort: {"title": 1}}
])
.limit(25)
.then((records) => res.send(records))
} else {
team.aggregate([
{$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
{$project: {"title" :
{ $concat : [
"$title",
" (",
{ $arrayElemAt:["$leader.email", 0] },
")"
] }
}},
{$sort: {"title": 1}}
])
.limit(25)
.then((records) => res.send(records))
}
}
Lets drill down the aggregate action, as we cannot access virtuals inside mongoose queries we cannot pull this data from a populate() action and then pass it into our records in one so we do a look up connecting team to users via it’s assigned ‘leader’ field which will match the user field _id.
{$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
Next with the user connected to team we project or create the title value concatenating the email address to the end in brackets.
{$project: {"title" :
{ $concat : [
"$title",
" (",
{ $arrayElemAt:["$leader.email", 0] },
")"
] }
}},
Then we match the result to the API request value.
{$match: {"title": { "$regex": q, "$options": "i" }}},
Sort the results.
{$sort: {"title": 1}}
Giving us the end output: