I’m building a site search which searches two fields that I have merged into one array of objects, I only need to match the “title” value based upon user’s input, example of my array (collections.js):
export const items = [
{url: '/my-first-post/', title: 'My first post', type: 'Guide', id: 'opt-1'},
{url: '/my-first-tag/', title: 'My first tag', type: 'Tag', id: 'opt-2'},
]
Then in my main JS file I listen for an ‘input’ event:
import { items } from '/js/collections.js';
const search = document.querySelector('#sFilter');
const searchList = document.querySelector('.search__list');
let arrFiltered = [];
let listNodes;
search.addEventListener('input', (evt) => {
if (search.value) {
search.setAttribute('aria-expanded', 'true');
search.parentElement.setAttribute('data-expanded', 'true');
filterItems(search.value);
} else {
search.setAttribute('aria-expanded', 'false');
search.parentElement.setAttribute('data-expanded', 'false');
searchList.innerHTML = '';
}
})
Then my function is:
const filterItems = (term) => {
arrFiltered = items.filter(item =>
item.title.toLowerCase().trim().includes(term.toLowerCase().trim()));
arrFiltered.forEach((item, idx) => {
let res = `<li class="search__item" data-pos="${idx + 1}">
<a class="search__option" id="${item.id}" href="${item.url}" tabindex="-1">
<span class="underline">${item.title}</span><span class="search__type">${item.type}</span></a></li>`
searchList.insertAdjacentHTML('beforeend', res);
});
listNodes = document.querySelectorAll('.search__item');
// Remove DOM nodes not present in arrFiltered
}
What I am trying to do (unsure if going about it the best way), is:
- Keep the data in the object for looking up (Done)
- Compare the
search.value.toLowerCase().trim()
against theitem.title.toLowerCase().trim()
(Done) - Add to the
arrFiltered
array (Done) - Limit that array to a max of 10 results (TODO)
- Compare input value against
item.title
, ignoring punctuation:(/["'..,!?]/g, '')
&('-', ' ')
(In Progress) - Create a collection of
<li>
items (Done) and append these items to thesearchList
element (Done) - Remove or add those
<li>
els on subsequent inputs, as opposed to say settinginnerHTML = ''
on every new input event (TODO)
Obviously in my provided code there is no attempt to remove()
elements from the DOM that don’t appear in arrFiltered and I know I can set the innerHTML as an empty string on each input event, but that’s not suitable for this, as “focus” must be maintained on the currently focused element (primarily screen reader users), if that element is still in the arrFiltered
array, of course.
An example of my HTML:
<ul class="search__list" id="lBox">
<li class="search__item" data-pos="1">
<a class="search__option" id="opt-63" href="/posts/my-first-post/" tabindex="-1">
<span class="underline">My first post</span><span class="search__type">Post</span>
</a>
</li>
<li class="search__item" data-pos="2">
<a class="search__option" id="opt-64" href="/tags/my-first-tag/" tabindex="-1">
<span class="underline">My first tag</span><span class="search__type">Tag</span>
</a>
</li>
</ul>
So, my question is (finally), I need to remove()
DOM elements that are no longer in the arrFiltered
array, so I know I need to use a method that will compare an ID present in the nodeList (the <li>.firstElementChild.id
) against item.id
in the arrFiltered
array, what would be a good way of doing this? I have tried map()
, filter()
, [...spread]
and Array.from()
, with little success, I’m not saying one or more of those doesn’t work, just I’m obviously doing it wrong :/ so i’d be grateful for a little steer. thank you 🙂