1

I have an application where the user types in text, then this text is sent to the server and it returns an array of words that contain this text.

enter image description here

But as you can see the problem starts when there are more then 1 match. Here's my current code:

state.input !== '' && vocabularyItems && (vocabularyItems.map((vocabularyItem, index) => {
      const regex = new RegExp(input, 'gi');
      const results = vocabularyItem.matchAll(regex);

      const tmp = [];
      console.log(vocabularyItem);
      for (const match of results) {
        console.log(match);
        let currentLoop = vocabularyItem.slice(0, match.index);

        currentLoop += '<strong className="tt-highlight">';

        currentLoop += vocabularyItem.slice(match.index, match.index + input.length);

        currentLoop += '</strong>';

        currentLoop += vocabularyItem.slice(match.index + input.length, vocabularyItem.length);

        tmp.push(currentLoop);
      }
      console.table(tmp);

      return (
        <div
          id={index}
          className={`override-strong tt-suggestion tt-selectable ${cursor === index && 'tt-cursor'}`}
          onMouseDown={handleClick}
          key={index}
          dangerouslySetInnerHTML={{ __html: tmp }}
        />
      );
    }))

and here are some examples in HTML code

1.
<strong className="tt-highlight">En</strong>kelkind

2.
<strong className="tt-highlight">En</strong>gagement
Engagem<strong className="tt-highlight">en</strong>t

as you can see, it works when there is only one occurence, but duplicates the word when more then one match is present. How can I end up with just something like

<strong>en</strong>gagem<strong>en</strong>t?

engagement?

I forgot to add that I need the case preserved

0

3 Answers 3

3

First off, I would recommend to use, something, like:

const results = vocabularyItems.filter(word => word.toLowerCase().includes(input.toLowerCase()))

for case insensitive vocabulary lookup.

Next, I would highlight the match in a bit different way. I'd split suggested option into parts (matching search input and non-matching ones) , then style those respectively:

const parts = suggestion.split(new RegExp(`(?=${match})|(?<=${match})`, 'gi'))
...
parts.map((part,key) => {
          const style = part.toLowerCase() == match.toLowerCase() ? 'bold' : 'normal'
          return <span style={{fontWeight:style}} {...{key}}>{part}</span>
        })

I think it's safe enough to assume that you build autocomplete search input, thus you might find of use the quick demo below (excluding all the styling):

//dependencies
const { render } = ReactDOM,
      { useState } = React

//vocabulary      
const vocabulary = ['engagement', 'Bentley', 'English', 'seven', 'Engagement']

//suggested option component
const SuggestedOption = ({suggestion, match}) => {
  const parts = suggestion.split(new RegExp(`(?=${match})|(?<=${match})`, 'gi'))
  return (
    <div>
      {
        parts.map((part,key) => {
          const style = part.toLowerCase() == match.toLowerCase() ? 'bold' : 'normal'
          return <span style={{fontWeight:style}} {...{key}}>{part}</span>
        })
      }
    </div>
  )
}

//autocomplete component
const SearchBar = () => {
  const [suggestions, setSuggestions] = useState([]),
        [inputValue, setInputValue] = useState(''),
        onInput = input => {
          setInputValue(input)
          setSuggestions(vocabulary.filter(word => input.length && word.toLowerCase().includes(input.toLowerCase())))
        }
  return (
    <div>
      <input onKeyUp={e => onInput(e.target.value)} />
      <div >
        {
          suggestions.map((suggestion,key) => <SuggestedOption {...{key,suggestion,match:inputValue}} />)
        }
      </div>
    </div>
  )
}

render(
  <SearchBar />,
  document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

Sign up to request clarification or add additional context in comments.

7 Comments

How would you extend this to prevent Engagement and engagement appearing twice?
I forgot to add that I need the case to be preserved
if the server responds with Engagement i need to highlight it like <strong>En</strong>gagem<strong>en</strong>t. This is under the condition that the user typed in en, or En
It returns whatever it finds in the db
Ok I probably did something wrong. Where should I use this code?
|
3

Here's a way to do it with string.replace:

const list = [
  'end',
  'England',
  'engagement'
]

const boldify = (search) => {
  return (string) =>
    string.replace(new RegExp(search, 'gi'), '<strong>$&</strong>')
}
  

document.body.innerHTML = list.map(boldify('en')).join('<br>')


EDIT: After some times thinking, and seeing the accepted answer, I wanted to push the vanilla JS version to have something more complete. And moreover, the React version seemed laggy to me, this one is faster!

Faster because:

  • it uses vanilla JavaScript
  • it uses correct dom manipulation (no innerHTML)

'use strict'

const list = [
  'end',
  'England',
  'engagement',
  'Ken Bogard',
  'Venom',
  'Engagement'
]

const boldify = (search) => {
  return (string) => {
  	const div = document.createElement('div')
  	const parts = string.split(new RegExp(`(?=${search})|(?<=${search})`, 'gi'))
    div.append(
      ...parts.map(part => {
    	  if (part.toUpperCase() !== search.toUpperCase()) return part
      
        const strong = document.createElement('strong')
        strong.innerText = part
        return strong
      })
    )
    return div
  }
}

const contains = (search) => {
  search = search.toUpperCase()
  return (string) => string.toUpperCase().indexOf(search) >= 0
}

const refreshSuggestions = () => {
  // Fast removal of children.
  while (suggestions.firstChild) suggestions.removeChild(suggestions.firstChild)

  // nothing to do
  if (searchInput.value.length == 0) return
  
  const newSuggestions =
    list.filter(contains(searchInput.value))
        .map(boldify(searchInput.value))
  suggestions.append(...newSuggestions)
}

searchInput.addEventListener('input', refreshSuggestions)
<input id="searchInput" list="suggestions">
<div id="suggestions">
</div>

8 Comments

What is the $& in the strong?
Taking into account reactjs tag, I think, with this approach (that is very similar to the one I've suggested at first) it may be difficult to render modified string within React component, as it will render part of a string enclosed within <strong> tag as [Object][object]. I came across that problem while preparing live-demo for my answer so I switched to another one.
@YevgenGorbunkov indeed your solution is more thorough. I didn't even see the reactjs tag and went for a first step solution that OP would have to enhance to his desire :) I'll still let my solution here, guessing that it may interest one for a quick solution! But of course I do not recommand generating HTML with text strings.
@YevgenGorbunkov I've updated my answer, this version is blazing fast and I thought you would like to see it :)
|
1

EDIT - Yevgen's answer is much nicer than this one.

A simple loop can achieve this if i understand correctly what you want:

var array = ["end","engagement","Engagement","england","enough","not this","or this"];

function filterArray(array, id) {
    var returnArray = [];
  for (var i = 0; i < array.length; i++) {
    value = array[i];
    if (value.includes(id)) {
        returnArray.push(value);
    }
    }
    return returnArray;
}

var filteredArray = filterArray(array,"en");
console.log(filteredArray);

If you wanted to ensure duplicates (where case on Engagement causes a duplicate) you could set the string to lowercase before pushing to the array, and check the array for the existence of the string before pushing again.

1 Comment

pushing lowercased words into suggestions array wouldn't appear very nice for items like English, I'd rather suggest to keep case untouched, though it may eat up performance a bit. You may check out example over here (lines 29 through 34, test keywords 'Engagement' and 'seven').

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.