Semantic UI Dropdown with Remote Data
Adam C. |

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. 

Photo by Clay Banks on Unsplash

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. 

Semantic UI Dropdown

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 after wait milliseconds have elapsed since the last time the debounced function was invoked. The debounced function comes with a cancel method to cancel delayed func invocations and a flush method to immediately invoke them. Provide options to indicate whether func should be invoked on the leading and/or trailing edge of the wait timeout. The func is invoked with the last arguments provided to the debounced function. Subsequent calls to the debounced function return the result of the last func 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.

FeathersJS Search API

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.

  • add whitelist to enable some special params in src/servcies/tags/tags.service.js
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);
};
  • add search hook in src/services/tags/tags.hook.js
const search = require("feathers-mongodb-fuzzy-search");

module.exports = {
  before: {
    all: [],
    find: [search({ fields: ["name", "slug"] })],
    ...
  • mutate param.query in src/services/tags/tags.class.js

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.