3

I have made a simple select component in Vue with a search/filter system. Based on the user input I'm showing some Belgium city suggestions.

Working example: https://codesandbox.io/s/wandering-lake-lecok?file=/src/components/Select.vue (Sometimes there is an error message in Codesandbox. Refresh the build in browser and it should work)

I want to take the UX one step further and show the matching part of the user input bold and underlined. Therefore I have a working makeBold function. By splitting the suggestion string into multiple parts I can add a bold and underline tag and return the suggestion.

computed: {
    results() {
        return this.options.filter((option) =>
        option.display_name
            .replaceAll("-'", "")
            .toLowerCase()
            .includes(this.searchInput.replaceAll("-'", "").toLowerCase())
        );
    },
},
methods: {
    makeBold(str, query) {
        const n = str.toUpperCase();
        const q = query.toUpperCase();
        const x = n.indexOf(q);

        if (!q || x === -1) {
            return str;
        }

        const l = q.length;

        return (
            str.substr(0, x) + "<b><u>" + str.substr(x, l) + "</u></b>" + str.substr(x + l)
        );
    },
}

One problem, a lot of cities in Belgium use dashes and/or apostrophes. In the suggestions function I'm removing this characters so a user doesn't need to type them. But in the makeBold function I would like to make this characters bold and underlined.

For example:

When the input is 'sint j', 'sintj' or 'Sint-j' I want the suggestions to look like 'Sint-Jans-Molenbeek' and 'Sint-Job in't Goor'

Is there someone who can give me a breakdown on how to achieve this?

Thore
  • 1,918
  • 2
  • 25
  • 50

3 Answers3

2

I would propose using a mask, to save the city name structure, and after you find the start and end index of substring in city name, restore the original string from mask, inserting the appropriate tags at the start and end index using a replacer function. this way you would not worry about any other non-word characters or other unexpected user input.

Here is the makeBold function:

makeBold(str, query) {
      // mask all word characters in city name
      const city_mask = str.replace(/\w/g, "#");
      // strip city and query string from any non-word character
      let query_stripped = query.toLowerCase().replace(/\W/g, "");
      let string_stripped = str.replace(/\W/g, "");
      // find the index of querystring in city name
      let index = string_stripped.toLowerCase().indexOf(query_stripped);

      if (index > -1 && query_stripped.length) {
        // find the end position of substring in stripped city name
        let end_index = index + query_stripped.length - 1;

        // replacer function for each masked character.
        // it will add to the start and end character of substring the corresponding tags,
        // replacing all masked characters with the original one.
        function replacer(i) {
          let repl = string_stripped[i];
          if (i === index) {
            repl = "<b><u>" + repl;
          }
          if (i === end_index) {
            repl = repl + "</u></b>";
          }
          return repl;
        }
        let i = -1;
        // restore masked string
        return city_mask.replace(/#/g, (_) => {
          i++;
          return replacer(i);
        });
      }

      return str;
    }

And here is the working sandbox. I've changed a bit your computed results to strip all non-word characters.

Igor Moraru
  • 7,089
  • 1
  • 12
  • 24
1

One way is to transform your search string into a RegExp object and use replace(regexp, replacerFunction) overload of string to achieve this.
For example the search string is "sintg"

new RegExp(this.searchInput.split("").join("-?"), "i");

Turns it into /s-?i-?n-?t-?g/gi
-? indicates optional - character and
"i" at the end is the RegExp case insensitive flag

Applied to codesandbox code you get this

computed: {
  results() {
    const regex = new RegExp(this.searchInput.split("").join("-?"), "i");
    return this.options.filter((option) => option.display_name.match(regex));
  },
},
methods: {
  makeBold(str, query) {
    const regex = new RegExp(query.split("").join("-?"), "i");
    return str.replace(regex, (match) => "<b><u>" + match + "</u></b>");
  },
},

Which gives this result

enter image description here

However there is a caveat: There will be errors thrown if the user puts a RegExp special symbol in the search box
To avoid this the initial search input text needs to get RegExp escape applied.
Such as:

new RegExp(escapeRegExp(this.searchInput).split("").join("-?"), "i");

But there is no native escapeRegExp method.
You can find one in Escape string for use in Javascript regex
There is also an escapeRegExp function in lodash library if it's already in your list of dependencies (saves you from adding another function)

Glass Cannon
  • 394
  • 3
  • 9
0

You could create a function that removes all spaces and - in the query and city string. If the city includes the query, split the query string on the last letter and get the occurences of that letter in the query. Calculate the length to slice and return the matching part of the original city string.

const findMatch = (q, c) => {
  const query = q.toLowerCase().replace(/[\s-]/g, "");
  const city = c.toLowerCase().replace(/[\s-]/g, "");
  
  if (city.includes(query)) {
    const last = query.charAt(query.length - 1); // last letter
    const occ = query.split(last).length - 1; // get occurences
    // calculate slice length
    const len = c.toLowerCase().split(last, occ).join(" ").length + 1;
    return c.slice(0, len);
  }

  return "No matching city found."
}

const city = "Sint-Jan Test";
console.log(findMatch("sint j", city));
console.log(findMatch("sintj", city));
console.log(findMatch("Sint Jan t", city));
console.log(findMatch("sint-j", city));
console.log(findMatch("Sint-J", city));
console.log(findMatch("SintJan te", city));
axtck
  • 3,707
  • 2
  • 10
  • 26