The blog system of DeniApps has a field named Tags. When writing a new post, I can either create a new one or choose the existing one. As more tags were added into the system, one day, I found that I could not find the tag 'React' which I am pretty sure is in the system. I looked into this issue and realized that the Tags dropdown options are populated when the page is rendered. Due to the default pagination size of FeathersJS, only 10 records are fetched. Since I have way more than 10 tags in the system, not all tags are available.
One simple solution is increasing the page size, but I would like to do something better. Semantic UI Dropdown supports a prop named onSearchChange
, where we can create a handler to call the backend API to get the real-time options based on the search query.
Here is the fronted change I made in the Dropdown Component:
<Dropdown
placeholder={props.placeholder || "Please choose"}
name={props.name}
selection
search
options={props.options}
multiple
value={props.value ? props.value : []}
allowAdditions
onChange={handleDropdownChange}
onSearchChange={handleSearchChange}
onAddItem={handleAddition}
/>;
const handleSearchChange = (event, search) => {
if (props.handleSearchChange) {
props.handleSearchChange(search.searchQuery);
}
};
As you see, the handleSearchChange will pass the searchQuery to the function in the parent component:
handleSearchChange = (kw) => {
this.debounceFun(kw);
};
debounceFun = _.debounce(async (kw) => {
console.log(kw);
const ret = await searchTags(kw);
const matchedTags = ret.data;
const tagsInputOptions = matchedTags.map((item) => ({
value: item.slug,
text: item.name,
key: item._id
}));
const newAllOptions = {
...this.state.allOptions,
tags: tagsInputOptions
};
this.setState({ allOptions: newAllOptions });
}, 500);
Nothing special here, but I use lodash.debounce to prevent too many requests from killing the backend server.
Creates a debounced function that delays invoking
func
until afterwait
milliseconds have elapsed since the last time the debounced function was invoked. The debounced function comes with acancel
method to cancel delayedfunc
invocations and aflush
method to immediately invoke them. Provideoptions
to indicate whetherfunc
should be invoked on the leading and/or trailing edge of thewait
timeout. Thefunc
is invoked with the last arguments provided to the debounced function. Subsequent calls to the debounced function return the result of the lastfunc
invocation.
Learn more about debounce.
The searchTag
function is like below:
const searchTags = (kw) => {
return agent({
method: "get",
url: "/tags/?$search=" + encodeURIComponent(kw),
});
};
Note agent is the wrapper of API calls, here is the source code. And the “/tags/?$search
” is the API endpoint.
The DeniApps Blog system uses FeathersJS as a backend with MongoDB. To implement the search function, I use ‘feather-mongodb-fuzzy-search’, which supports both ‘full-text’ search and ‘filed pattern’ matching. For the tags search, I use the latter. - For example, when I type in ‘r’, I would expect any tags whose name starts with ‘r’, like ‘react’ returned. To implement this.
module.exports = function (app) {
const options = {
Model: createModel(app),
paginate: app.get("paginate"),
whitelist: ["$populate", "$text", "$search", "$regex"],
};
// Initialize our service with any options it requires
app.use("/tags", new Tags(options, app));
// Get our initialized service so that we can register hooks
const service = app.service("tags");
service.hooks(hooks);
};
const search = require("feathers-mongodb-fuzzy-search");
module.exports = {
before: {
all: [],
find: [search({ fields: ["name", "slug"] })],
...
I thought this should be handled by search hook, but I got this error because I mutated the param.query.
error: MongoError: unknown top level operator: $regex. If you have a field name that starts with a '$' symbol, consider using $getField or $setField.
And then, I found that the query after search hook is something like:
query: { '$regex': /r/i },
By look into MongoDB doc, the correct syntax should be:
{ <field>: { $regex: /pattern/<options> } }
So I have to update the query in tags.class.js as below:
exports.Tags = class Tags extends Service {
async find(params) {
if (params.query.$regex) {
params.query = {
name: params.query,
};
}
return super.find(params);
}
};
Note: name
is the field of tags table, where I would like to search for.