Gatting started

Include JS and CSS files

The first thing you should do is download the autocomplete library files. All the necessary files are located in the "docs/js" and "docs/css" folders. If you want to make changes, the uncompiled javascript and sass files are in the sources folder.

// CSS file
<link rel="stylesheet" href="path/to/autocomplete.css">

// JS file
<script src="path/to/autocomplete.min.js"></script>

If you want on older browsers - IE use polyfills

<script>
  if (!('Promise' in window)) {
    var script = document.createElement('script');
    script.src =
      'https://polyfill.io/v3/polyfill.min.js?features=Promise%2CElement.prototype.closest';
    document.getElementsByTagName('head')[0].appendChild(script);
  }
</script>
-- OR --
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-closest@3.0.2/browser.min.js"></script>
-- OR --
<script src="path/to/polyfill.js"></script>

Configuration of the plugin:

Settings

Property Default Required Description
element Input field id
clearButton false A parameter set to 'true' adds a button to remove text from the input field. You can also use the destroy method - description below
selectFirst false Default selects the first item in the list of results
insertToInput false Adding an element selected with arrows to the input field field
disableCloseOnSelect false Prevents results from hiding after clicking on an item from the list
classPreventClosing "" Prevents results from hiding after clicking on element with this class. See the Footer/Header
cache false The characters entered in the input field are cached
howManyCharacters 1 The number of characters entered should start searching
delay 500 Time in milliseconds that the component should wait after last keystroke before calling search function 1000 = 1s
classGroup "" Enter a class name, this class will be added to the group name elements
Callbacks function Required Description
onSearch: ({ currentValue, element }) => {} Function for user input. It can be a synchronous function or a promise
onResults: ({ currentValue, matches, template, classGroup }) => {} when we want to add the information No results found: we need to do a comparison, check the matches is 0 then set the template return matches === 0 ? template : matches
onRender: ({ element, results }) => {} when we want to add additional elements, e.g. some buttons or plain text. See the Footer/Header example. This function is only called once.
onSubmit: ({ index, element, object, results }) => {} Executed on input submission
onOpened: ({ type, element, results }) => {} Returns two variables results and showItems.
resutls first rendering of the results showItems only showing the results when clicking on the input field
onSelectedItem: ({ index, element, object }) => {} Get index and data from li element after hovering over li with the mouse or using arrow keys |
onReset: (element) => {} After clicking the x button
onClose: () => {} e.g. remove class after closing results. See an example of modal
noResults: ({ element, currentValue, template }) => {} Showing information: "no results"
Public methods Description
destroy Removes the autocomplete instance and its bindings,
how to use: const auto = new Autocomplete('id', {}); auto.destroy();

Explanation of the variables

Function params Description
index Record number in the results
currentValue Data entered into the input field
element Input field for data entry
matches An array of elements matching the entered word
object Object from the query
type Can return two results results or showItems.
results occurs each time a new result appears, showItems occurs when we click on the input element and the result reappears
results The entire ul + li html element is returned
template Create an element to display information about no results

Basic example

In fact, the input <input type="text" id="basic" placeholder="type w"> field is enough to add a search engine. The div surrounding the input field controls the appearance of this field and also the appearance of the "loupe" icons and the button with which we can clear the x field, as well as the possibility of an animation waiting for the results before our REST API returns us.

              new Autocomplete('basic', {
  onSearch: ({ currentValue }) => {
    const api = `https://breakingbadapi.com/api/characters?name=${encodeURI(currentValue)}`;
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          resolve(data);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  }
});
            

Complex example

search - this class is responsible for the appearance of the input field

max-height - this class is responsible for the maximum height of the div in which the results appear, if there are many results, we can also scroll using the up/down arrows

loupe - this class shows a "loupe" icon that appears in the input field on the left

              const auto = new Autocomplete('complex', {
  // search delay
  delay: 1000,

  // add button 'x' to clear the text from
  // the input filed
  clearButton: false,

  // default selects the first item in
  // the list of results
  selectFirst: true,

  // add text to the input field as you move through
  // the results with the up/down cursors
  insertToInput: true,

  // the number of characters entered
  // should start searching
  howManyCharacters: 1,

  // the characters entered in
  // the input field are cached
  cache: true,

  // enter the name of the class by
  // which you will name the group element
  classGroup: 'group-by',

  // Function for user input. It can be a synchronous function or a promise
  // you can fetch data with jquery, axios, fetch, etc.
  onSearch: ({ currentValue }) => {
    // static file
    // const api = './characters.json';

    // OR -------------------------------

    // your REST API
    const api = `https://breakingbadapi.com/api/characters?name=${encodeURI(currentValue)}`;
    /**
      * jquery
      * If you want to use jquery you have to add the
      * jquery library to head html
      * https://cdnjs.com/libraries/jquery
      */
    // return $.ajax({
    //   url: api,
    //   method: 'GET',
    // })
    //   .done(function (data) {
    //     return data
    //   })
    //   .fail(function (xhr) {
    //     console.error(xhr);
    //   });

    // OR ----------------------------------

    /**
      * axios
      * If you want to use axios you have to add the
      * axios library to head html
      * https://cdnjs.com/libraries/axios
      */
    // return axios.get(api)
    //   .then((response) => {
    //     return response.data;
    //   })
    //   .catch(error => {
    //     console.log(error);
    //   });

    // OR ----------------------------------

    /**
      * Promise
      */
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          resolve(data);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  // this part is responsible for the number of records,
  // the appearance of li elements and it really depends
  // on you how it will look
  onResults: ({ currentValue, matches, template, classGroup, }) => {
    // const regex = new RegExp(^${input}`, 'gi'); // start with
    const regex = new RegExp(currentValue, 'gi');

    // counting status elements
    function count(status) {
      let count = {};
      matches.map(el => {
        count[el.status] = (count[el.status] || 0) + 1;
      });
      return `<small>${count[status]} items</small>`;
    }

    // checking if we have results if we don't
    // take data from the noResults method
    return matches === 0 ? template : matches
      .sort((a, b) => a.status.localeCompare(b.status) || a.name.localeCompare(b.name))
      .map((el, index, array) => {
        // we create an element of the group
        let group = el.status !== array[index - 1]?.status
          ? `<li class="${classGroup}">${el.status} ${count(el.status)}</li>`
          : '';

        // this part is responsible for the appearance
        // in the drop-down list - see the example in index.html
        // remember only the first element from <li> is put
        // into the input field, in this case the text
        // from the <p> element
        return `
          ${group}
          <li>
            <h2 style="margin-bottom: 10px;">
              ${el.name.replace(regex, (str) => `<b style="color: red;">${str}</b>`)}
            </h2>
            <div style="display: flex;">
              <div style="margin-right: 10px;">
                <img src="${el.img}" style="max-width: 67px;max-height:95px">
              </div>
              <div class="info">
                <h4>${el.name}</h4>
                <div><b>nickname:</b> - ${el.nickname}</div>
                <div><b>birthday:</b> - ${el.birthday}</div>
                <div><b>status:</b> - ${el.status}</div>
              </div>
            </div>
          </li>`;
      }).join('');
  },

  // the onSubmit function is executed when the user
  // submits their result by either selecting a result
  // from the list, or pressing enter or mouse button
  onSubmit: ({ index, element, object, results }) => {
    console.log('complex: ', index, element, object, results);
    // window.open(`https://www.imdb.com/find?q=${encodeURI(element.value)}`)
  },

  // get index and data from li element after
  // hovering over li with the mouse or using
  // arrow keys ↓ | ↑
  onSelectedItem: ({ index, element, object }) => {
    console.log('onSelectedItem:', index, element.value, object);
  },

  // the method presents no results
  noResults: ({ element, template }) => {
    template(`<li>No results found: "${element.value}"</li>`);
  }
});

// use destroy() method
const clearButton = document.querySelector('.clear-button');
clearButton.addEventListener('click', () => {
  auto.destroy();
});


            

No results

Adding no result is a bit more complicated, so it is included in the examples. First, add the noResults method, then in the onResults method we check whether maches returns zero results, if so, it is enough to make a comparison of return matches === 0 ? template : matches

Now you just need to type in non-existent text, and no results will be displayed, only "no results" information.

      new Autocomplete('no-results', {
  onSearch: ({ currentValue }) => {
    const api = './characters.json';
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          const result = data.sort((a, b) => a.name.localeCompare(b.name))
            .filter(element => {
              return element.name.match(new RegExp(currentValue, 'gi'))
            })
          resolve(result);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ matches, template }) => {
    // checking if we have results if we don't
    // take data from the noResults method
    return matches === 0 ? template : matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  },

  noResults: ({ element, template }) => template(`<li>No results found: "${element.value}"</li>`)
});
    

Static file

Instead of the REST API, you can use a static files const api='./characters.json'Due to the fact that we download the entire json file locally, we should add sorting and filtering the results.

We can add sorting and filtering in two ways. In the onSearch or onResults method.

              new Autocomplete('static', {
  // onSearch
  onSearch: ({ currentValue }) => {
    // static file
    const api = './characters.json';

    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          const result = data.sort((a, b) => a.name.localeCompare(b.name))
            .filter(element => {
              return element.name.match(new RegExp(currentValue, 'gi'))
            })
          resolve(result);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ currentValue, matches }) => {
    return matches
      .map(el => {
        return `
          <li class="loupe">
            <p>${el.name.replace(new RegExp(currentValue, 'gi'), (str) => `<b>${str}</b>`)}</p>
          </li>`;
      })
      .join('');
  }
});
            

Static file + data-elements

You want to download all the data, nothing easier. You can do it in the onSubmit method by clicking the mouse on the element, or by pressing enter when the element is selected, but also in the onSelectedItem method.

That is, selecting an element, either with the mouse or by jumping on the elements with the arrows.

Open the console and see what happens during these events.

              new Autocomplete('static-file-data', {
  selectFirst: true,

  // onSearch
  onSearch: ({ currentValue }) => {
    // static file
    const api = './characters.json';

    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          const result = data
            .sort((a, b) => a.name.localeCompare(b.name))
            .filter((element) => {
              return element.name.match(new RegExp(currentValue, 'gi'));
            });
          resolve(result);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ currentValue, matches }) => {
    return matches.map(({ name, status }) => {
      return `
        <li class="loupe">
          <p>${name.replace(new RegExp(currentValue, 'gi'), (str) => `<b>${str}</b>`)}</p>
          <small>status - ${status}</small>
        </li>`;
    }).join('');
  },

  // event onsubmit
  onSubmit: ({ index, element, object }) => {
    const { name, status, img } = object;

    console.table('static-file-data', index, element, object);

    const template = `
      <p>name - ${name}</p>
      <p>status - ${status}</p>
      <div class="image"><img src="${img}"></div>`;

    const info = document.querySelector('.info-d');
    info.classList.add('active-data');
    info.innerHTML = template;
  },

  // get index and data from li element after
  // hovering over li with the mouse
  onSelectedItem: ({ index, element, object }) => {
    console.log('onSelectedItem:', index, element.value, object);
  },

});
            

Data grouping

First, we declare the class of group members classGroup: 'group-by'

Then we sort by our group, in our case it will be status ['Alive', 'Deceased', 'Presumed dead', 'Unknown'], then we sort by name. Of course, it is not always needed, because we should get such soroting from our api 'REST API'

The next thing is the onResults method which returns the third element of the group class name which will prevent moving between result records with the arrow and with the mouse. This class should be added to the group items.

      new Autocomplete('group', {
  // enter a class name, this class will
  // be added to the group name elements
  classGroup: 'group-by',

  onSearch: ({ currentValue }) => {
    const api = './characters.json';
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {

          // first, we sort by our group, in our case
          // it will be the status, then we sort by name
          // of course, it is not always necessary because
          // such soroting may be obtained from REST API
          const result = data
            .sort((a, b) => a.status.localeCompare(b.status) || a.name.localeCompare(b.name))
            .filter(element => {
              return element.name.match(new RegExp(currentValue, 'gi'))
            })
          resolve(result);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ currentValue, matches, template, classGroup }) => {

    // counting status elements
    function count(status) {
      let count = {};
      matches.map(el => {
        count[el.status] = (count[el.status] || 0) + 1;
      });
      return `<small>${count[status]} items</small>`;
    }

    return matches === 0 ? template : matches
      .map((el, index, array) => {

        // we create an element of the group
        let group = el.status !== array[index - 1]?.status
          ? `<li class="${classGroup}">${el.status} ${count(el.status)}</li>`
          : '';

        return `
          ${group}
          <li class="loupe">
            <p>${el.name.replace(new RegExp(currentValue, 'gi'), (str) => `<b>${str}</b>`)}</p>
          </li>`;
      }).join('');
  },

  noResults: ({ currentValue, template }) => template(`<li>No results found: "${currentValue}"</li>`),
});
    

Dynamic list position

Dynamic display of results, and precisely the position of these results. The simplest thing is that the results, if they do not fit in the page view, under the input field, will be displayed above this field.

Move the page so that the input field is near the bottom of the window. Do a search, the results should appear above the input field.

      // displaying the results above the search field
new Autocomplete('dynamic-list-position', {
  insertToInput: true,
  cache: true,

  onSearch: ({ currentValue }) => {

    // local data
    const data = [
      { "name": "Walter White" },
      { "name": "Jesse Pinkman" },
      { "name": "Skyler White" },
      { "name": "Walter White Jr." }
    ];
    return data.sort((a, b) => a.name.localeCompare(b.name))
      .filter(element => {
        return element.name.match(new RegExp(currentValue, 'i'))
      })
  },

  onOpened: ({ element, results }) => {
    // we check if there is room for input for results
    const position = element.getBoundingClientRect().bottom + results.getBoundingClientRect().height > (window.innerHeight || document.documentElement.clientHeight);

    // if there is no room for results under the input field 
    // in the page view, we raise the results above the input field
    if (position) {
      results.style.bottom = element.offsetHeight - 1 + 'px';
    } else {
      results.removeAttribute('style');
    }

    // when checking the parent element, we also add a class based
    // on which we format the appearance of the results and their posture
    results.parentNode.classList[position ? 'add' : 'remove']('auto-list-up');
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  }
});
    

Number of records from the result

This example shows only the top 7 records of the total number of matched results.
For example, when searching for 'w', it would find 13 records but only 7 records are shown.

                      // global 'results'
let results = 0;
const maxRecords = 7;

new Autocomplete('records-result', {
  insertToInput: true,
  cache: true,
  classGroup: 'group-by',

  onSearch: ({ currentValue }) => {
    // clear array always when new searching
    results = [];

    const api = './characters.json';
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {

          const result = data
            .sort((a, b) => a.name.localeCompare(b.name))
            .filter((element) => {
              return element.name.match(new RegExp(currentValue, 'gi'));
            });

          // show only 7 records
          resolve(result.slice(0, maxRecords));

          // we set the number of record
          // to a global variable
          results = result.length;
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ currentValue, matches, template, classGroup }) => {
    return matches === 0
      ? template
      : matches
          .map((el, index, array) => {
            let resultsCount =
              index === 0
                ? `<li class="${classGroup}">
                    <span>Displaying
                      <strong>${
                        maxRecords > matches.length
                          ? matches.length
                          : maxRecords
                      }</strong>
                      from
                      <strong>${results}</strong>
                    </span>
                  </li>`
                : '';

            return `
              ${resultsCount}
              <li class="icon loupe">
                <p>${el.name.replace(
                  new RegExp(currentValue, 'gi'),
                  (str) => `<b>${str}</b>`
                )}</p>
              </li>`;
          })
          .join('');
  },

  onSelectedItem: ({ index, element, object }) => {
    console.log(index, element, object);
  },

  noResults: ({ currentValue, template }) =>
    template(`<li>No results found: "${currentValue}"</li>`),
});
                    

Local data

Not only json file or rest api, but also data can be downloaded locally. Also in this case, you need to sort and filter the data.

      new Autocomplete('local', {
  onSearch: ({ currentValue }) => {

    // local data
    const data = [
      { "name": "Walter White" },
      { "name": "Jesse Pinkman" },
      { "name": "Skyler White" },
      { "name": "Walter White Jr." }
    ];
    return data.sort((a, b) => a.name.localeCompare(b.name))
      .filter(element => {
        return element.name.match(new RegExp(currentValue, 'i'))
      })
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  }
});
    

Update input field on selected items

Adding this parameter adds the selected data to the input field while navigating through the records with the up/down arrows insertToInput: true

If you want to be able to add data to the input field after hovering the mouse over the record, you can do it using callback funciton onSelectedItem: ({ element, object }) => {element.value = object.name}

      new Autocomplete('update-input', {
  insertToInput: true,
  cache: true,

  onSearch: ({ currentValue }) => {
    // local data
    const data = [
      { "name": "Walter White" },
      { "name": "Jesse Pinkman" },
      { "name": "Skyler White" },
      { "name": "Walter White Jr." }
    ];
    return data.sort((a, b) => a.name.localeCompare(b.name))
      .filter(element => {
        return element.name.match(new RegExp(currentValue, 'i'))
      })
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  },

  onSelectedItem: ({ element, object }) => {
    element.value = object.name
  }
});
    

Select multiple values ver 1

This example allows you to select multiple elements that are added below the input field. To remove all elements just click button clear x or delete elements one by one by clicking the button x which are next to the name of the taken elements.

              let firstArray = [];
const countNumber = document.querySelector('.count-number');

new Autocomplete('select', {
  onSearch: ({ currentValue }) => {
    const api = './characters.json';
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          // first, we sort by our group, in our case
          // it will be the status, then we sort by name
          // of course, it is not always necessary because
          // such soroting may be obtained from REST API
          const result = data
            .sort((a, b) => a.name.localeCompare(b.name))
            .filter(element => {
              return element.name.match(new RegExp(currentValue, 'gi'))
            })
          resolve(result);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  },

  onOpened: ({ results }) => {
    // if the elements from the 'array' are identical to those
    // from the rendered elements add the 'selected' class
    [].slice.call(results.children).map(item => {
      if (firstArray.includes(item.textContent)) {
        item.classList.add('selected');
      }
    });
  },

  onSubmit: ({ element, results }) => {
    if (firstArray.includes(element.value)) {
      return;
    };

    // add the selected item to the array
    firstArray.push(element.value);

    // the place where we will add selected elements
    const selectedItem = document.querySelector('.selected-item');

    // create elements with names and buttons
    const button = document.createElement('button');
    button.type = 'button'
    button.className = 'remove-item';
    button.insertAdjacentHTML('beforeend', '<svg aria-label="Remove name" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/></svg>');

    const item = document.createElement('div');
    item.className = 'item';

    // add each item in the array to the div selectedItem
    firstArray.map(itemText => {
      item.textContent = itemText;
      item.insertAdjacentElement('beforeend', button);
      selectedItem.appendChild(item);
    });

    function setAttributeType(type) {
      [].slice.call(results.children).map(item => {
        if (item.textContent === button.parentNode.textContent) {
          item.classList[type === 'remove' ? 'remove' : 'add']('selected')
        }
      });
    }

    // update number count
    countNumber.textContent = firstArray.length;

    // remove selected element
    button.addEventListener('click', (e) => {
      e.stopPropagation();
      const parentElement = button.parentNode;

      // remove element from array
      firstArray.splice(firstArray.indexOf(parentElement.textContent), 1);

      // remove disabled attr
      setAttributeType('remove');

      // update number count
      countNumber.textContent = firstArray.length;

      // remove element from div
      parentElement.parentNode.removeChild(parentElement);
    });

    // add disabled attr
    setAttributeType();
  },

  onReset: (element) => {
    const selectedItem = document.querySelector('.selected-item');
    selectedItem.innerHTML = '';
    // after clicking the 'x' button,
    // clear the table
    firstArray = [];

    // remove count number
    countNumber.textContent = 0;
  }
});
            

Select multiple values ver 2

This example allows you to select multiple elements that are added to the input field, to remove the selected fields, clear them by pressing the clear x button or by removing the element or elements from the input field.

              // array initialization
let secondArray = [];
new Autocomplete('multiple-values', {
  onSearch: ({ element }) => {
    // first get all the items and split with a comma
    const lastElement = element.value.split(',').pop().trim();
    // if the last item is 0 then we don't do a search
    if (lastElement.length === 0) return;

    const data = [
      { "name": "Grzesiek" },
      { "name": "Andrzej" },
      { "name": "Monika" },
      { "name": "Wiesława" },
      { "name": "Waldemar" },
      { "name": "Włodzimierz" },
      { "name": "Adam" },
      { "name": "Agnieszka" },
      { "name": "Paweł" },
      { "name": "Tadeusz" },
      { "name": "Tymoteusz" },
      { "name": "Łucja" },
      { "name": "Nela" }
    ];
    return data.sort((a, b) => a.name.localeCompare(b.name))
      .filter(element => {
        return element.name.match(new RegExp(lastElement, 'gi'))
      })
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
          <li>${el.name}</li>`;
      }).join('');
  },

  onOpened: ({ element, results }) => {
    // type - two values 'results' and 'showItems',
    // 'resutls' first rendering of the results
    // 'showItems' only showing the results when clicking on the input field
    // resultList all results rendered containing ul and li
    // input - root input

    // get the data from the input field and divide by the
    // decimal point, then remove the empty last element
    const currentValue = element.value.split(', ').splice(0, element.value.length - 1);

    // leave in the array only those elements that are in the input field
    secondArray = secondArray.filter(el => currentValue.includes(el));

    // check if the table 'secondArray' contains selected elements from 
    // the input field, if so we add the 'selected' class to the 'li' element,
    // if not, remove the 'selected' class from the li element
    [].slice.call(results.children).map(item => {
      item.classList[secondArray.includes(item.textContent) ? 'add' : 'remove']('selected')
    });

  },

  onSubmit: ({ index, element, object, results }) => {
    if (secondArray.includes(element.value)) {
      return;
    };

    console.log('index: ', index, 'object: ', object, 'results: ', results);

    // each click on the li element adds data to the array
    secondArray.push(element.value.trim());

    // check if the table includes selected items from
    // the list, if so, add the 'selected' class
    [].slice.call(results.children).map(item => {
      if (secondArray.includes(item.textContent)) {
        item.classList.add('selected');
      }
    });

    // add the elements from the array separated by commas
    // to the 'input' field, also add a comma to the last element
    element.value = `${secondArray.join(', ')}${secondArray > 2 ? secondArray.pop()[secondArray.length - 1] : ', '}`;

    // after selecting an item, set the
    // focus to the input field
    element.focus();
  },

  onReset: (element) => {
    // after clicking the 'x' button,
    // clear the table
    secondArray = [];
  }
});
            

Checkboxes

This example shows what you can use the disableCloseOnSelect variable - checkbox list.

              let checkbox = [];
const countNumberCheckbox = document.querySelector('.count-number-checkbox');

// the place where we will add selected elements
const selectedItem = document.querySelector('.selected-item-checkbox');

new Autocomplete('checkbox', {
  // prevents results from hiding after 
  // clicking on an item from the list
  disableCloseOnSelect: true,

  onSearch: ({ currentValue }) => {
    const api = './language.json';
    return new Promise((resolve) => {
      fetch(api)
        .then((response) => response.json())
        .then((data) => {
          // first, we sort by our group, in our case
          // it will be the status, then we sort by name
          // of course, it is not always necessary because
          // such soroting may be obtained from REST API
          const result = data
            .sort((a, b) => a.name.localeCompare(b.name))
            .filter(element => {
              return element.name.match(new RegExp(currentValue, 'gi'))
            })
          resolve(result);
        })
        .catch((error) => {
          console.error(error);
        });
    });
  },

  onResults: ({ matches }) => {
    return matches
      .map(el => {
        return `
            <li class="custom-element">
              <label>
                <input type="checkbox" value="${el.name}">
                <div class="checkbox">${el.name}</div>
              </label>
            </li>`;
      }).join('');
  },

  onOpened: ({ results }) => {
    // if the elements from the 'array' are identical to those
    // from the rendered elements add the 'selected' class

    [].slice.call(results.children).map((item, idx) => {
      const test = checkbox.some(element => element === item.textContent.trim());
      if (!test) return;

      let inputElement = results.children[idx].firstElementChild.children[0];
      inputElement.checked = true;
      inputElement.closest('.custom-element').classList.add('checkbox-selected');
    });
  },

  onSubmit: ({ index, element, object, results }) => {
    // update counter elements
    function countNumber(numb) {
      return countNumberCheckbox.textContent = numb;
    }

    // remove element from array
    function removeItemFromArray(el) {
      let index = checkbox.indexOf(el);
      if (index > -1) {
        checkbox.splice(index, 1);
      }
    }

    function addRemoveClass(type) {
      inputElement.closest('.custom-element').classList[type]('checkbox-selected');
      // set false checbox
      inputElement.checked = type === 'remove' ? false : true;
    }

    let inputElement = results.children[index].firstElementChild.children[0];

    if (inputElement.checked) {
      // remove class 
      addRemoveClass('remove');

      // remove from array object
      removeItemFromArray(element.value);

      // update counter div
      countNumber(checkbox.length);

      // remove button
      [].slice.call(selectedItem.children).map(item => {
        if (item.textContent.trim() === element.value) {
          item.parentNode.removeChild(item);
        }
      })
      return;
    };

    // set checbox on true and add class
    addRemoveClass('add');

    // add the selected item to the array
    checkbox.push(element.value);

    // create elements with names and buttons
    const button = document.createElement('button');
    button.type = 'button'
    button.className = 'remove-item';
    button.insertAdjacentHTML('beforeend', '<svg aria-label="Remove name" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/></svg>');

    const item = document.createElement('div');
    item.className = 'item';

    // add each item in the array to the div selectedItem
    checkbox.map(ele => {
      item.textContent = ele;
      item.insertAdjacentElement('beforeend', button);
      selectedItem.appendChild(item);
    });

    // update number count
    countNumber(checkbox.length);

    // remove selected element
    button.addEventListener('click', (e) => {
      e.preventDefault();

      const element = e.target.closest('.item');

      // remove from array object
      removeItemFromArray(element.textContent);

      // update checkbox
      [].slice.call(results.children).map((item, idx) => {
        let inputElement = results.children[idx].firstElementChild.children[0];
        inputElement.checked = false;
        addRemoveClass('remove');
      });

      // update number count
      countNumber(checkbox.length);

      // remove element from div
      const parentElement = button.parentNode;
      parentElement.parentNode.removeChild(parentElement);
    });
  },

  onReset: (element) => {
    selectedItem.innerHTML = '';
    // after clicking the 'x' button,
    // clear the table
    checkbox = [];

    // remove count number
    countNumberCheckbox.textContent = 0;
  },

  noResults: ({ element, template }) => template(`No results found: "${element.value}"`)
});
              
            

Address geocoding

Below is an example of how to combine city geocoding with the AUTOCOMPLETE library. In fact, there are many ideas for using it ;)