Tag Archives: web components

Web Components: create text field with autosuggest dropdown and dictionary

What are web components?

Web components are new reusable components that add new functionalities to a standard components (ie: img, input, table, h, p, div, etc.) or create new components using and combining the existing standard components. You could read more here: web components

How to create custom web component ?

The simplest idea of how to create a web component is this: (although this could be highly customised, and done in many different ways, for the purpose of this exercise to be as simple as possible we will explore the simplest approach):

  1. Create custom class that will handle the presentational logic of the component.
  2. Add the custom tag in the document.
  3. Add the styles and the HTML markup to the main DOM. This is usually done by adding the shadow DOM of the new component to the document’s DOM.

Let’s make a real useful web component, while going to the process

Create a Text Field component with auto suggest drop down drawer and dictionary of the suggested words.

Final product will look like this:

Day:
Month:

First We will

Create the HTML and the CSS layout.

Create a new file

./text-box-with-dropdown.js

var template = `
<style>
  .wrapper {
    display: inline-grid;
  }
  #drawer {
    cursor: pointer;
    border: 1px solid silver;
    background: #f4f4f4;
  }
  .selectedRow {
    color: white;
    background: #606062;
  }
  p {
    margin: 1px;
    border-bottom: 1px solid silver;
  }
  p:last-child {
    border-bottom: none;
  }  
</style>

<div class="wrapper">
  <input type="text" id='textfield'>
  <div id='drawer'>
  </div>
</div>
`;

We added the CSS, and the HTML markup which consists of a wrapper div, a text field, and a ‘drawer’ div, which will show the suggested words.

Add web component’s class.

Next, In the same file create a class to handle the events.

class TextboxWithDropdown extends HTMLElement {  

  constructor() {
    // Always call super first in constructor
    super();
    this._shadowRoot = this.attachShadow({ 'mode': 'open' });
  }

  connectedCallback() {  
    this._shadowRoot.innerHTML = template; 
  }
}

And now let’s attach the newly created web component to the document DOM

window.customElements.define('textbox-with-dropdown', TextboxWithDropdown);

Now we are going to create the actual HTML document to view the component.

./index.html

<!DOCTYPE html>
<html lang="en" prefix="og=https://ogp.me/ns#" itemType="https://schema.org/WebPage" data-reactroot=""></html>
  <head>
    <script src="./text-box-with-dropdown1.js"></script>
    <style>
      body {
            padding: 40px;
            padding-bottom: 40px;
            background-color: #f5f5f5;
      }    
    </style>
  </head>

  <body>
    Day: <textbox-with-dropdown id="txt1" dictionary="1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31" value='1'></textbox-with-dropdown>
    Month: <textbox-with-dropdown id="txt2" dictionary="January,February,March,April,May,June,July,August,September,October,November,December"></textbox-with-dropdown>
  </body>
</html>

 

What we just did:
– we created simple HTML document
– Added two instances of our new WEB component textbox-with-dropdown
– Passed a property with a list of all days of the month to the first component and set it’s value=’1′ which will be the default parameter.
– Passed months of the year as a property to the second component.

So far the component will be visible but not quite functional. Let’s add all that we need to make the component functional.

First let’s discuss connectedCallback hook. It is invoked each time the custom element is appended into a document-connected element, while attributeChangedCallback is invoked when one of the custom element’s attributes is added, removed, or changed. How we are going to use this? We are going to move all initialization logic to connectedCallback
Then we are going to use attributeChangedCallback to listen to attribute changes and update the component. For example if you use the dev tools and inspect the component and edit the dictionary attribute, our component will update accordingly.

Next we are going to add a function that will accept a prefix parameter, which will be whatever the user starts typing in the text field and will filter the dictionary and return only matching words (or numbers).

We will also add keyUp events, so the user will be able to use ‘up’ and ‘down’ keys to navigate through the words list, and in addition we will add mousemove events to highlight the words where the mouse is.

Go back to text-box-with-dropdown.js and add these functionalities.

./text-box-with-dropdown.js

var template = `
<style>
  .wrapper {
    display: inline-grid;
  }
  #drawer {
    cursor: pointer;
    border: 1px solid silver;
    background: #f4f4f4;
  }
  .selectedRow {
    color: white;
    background: #606062;
  }
  p {
    margin: 1px;
    border-bottom: 1px solid silver;
  }
  p:last-child {
    border-bottom: none;
  }  
</style>

<div class="wrapper">
  <input type="text" id='textfield'>
  <div id='drawer'>
  </div>
</div>
`;

class TextboxWithDropdown extends HTMLElement {  

  constructor() {
    // Always call super first in constructor
    super();
    // and attach the component
    this._shadowRoot = this.attachShadow({ 'mode': 'open' });
  }

  connectedCallback() {
    // and when the component is mounted, do the rest to make it work
    this.selectedIndex = 0;
    this.filteredWordsCount = 0;
    this.isDrawerOpen = false;

    this._shadowRoot.innerHTML = template;
    this.textfield = this._shadowRoot.getElementById("textfield");
    this.dictionary = this.getAttribute("dictionary").split(',');
    this.textfield.value = this.getAttribute("value");
    this.textfield.addEventListener("keyup", function(e) {
      this.keyUp(e);
    }.bind(this));    
  }

  static get observedAttributes() {
    // on attributes changed by the browser dev tools this will reflect the changes
    return ["dictionary", "value"];
  }  

  get value() {
   // return the value
    return this.textfield.value;
  }

  attributeChangedCallback(name, oldValue, newValue) {
    //Custom square element attributes changed.
    this.dictionary = newValue.split(',');
  }

  selectHighlight(i) {
    this._shadowRoot.getElementById('row-' + this.selectedIndex).classList.remove("selectedRow");
    this._shadowRoot.getElementById('row-' + i).classList.add("selectedRow");
    this.selectedIndex = i;
  }

  keyUp(e) {
    if(e.keyCode == 13) {
      this.rowSelected(e);
      return;
    }
    if(e.keyCode == '38' || e.keyCode == '40') {
      this.arrowUpDown(e);
      return;
    }
    var prefix = this._shadowRoot.getElementById('textfield').value;
    if(prefix == '') {
      this._shadowRoot.getElementById('drawer').innerHTML = '';
      this.selectedIndex = 0;
      this.filteredWordsCount = 0;
      return;
    }
    if(this.isDrawerOpen == true)
      return;
    var words = this.filterWords(prefix);
    this._shadowRoot.getElementById('drawer').innerHTML = words.join('');
    // attach the events
    var c = 0;
    words.map(function(row, i) {
      var row = this._shadowRoot.getElementById('row-' + i);
      row.addEventListener("mousemove", function() {
        this.selectHighlight(i);
      }.bind(this));
      row.addEventListener("click", function(e) {
        this.rowSelected(e);
      }.bind(this));
    }.bind(this));
    // select first row if any
    if(words.length > 0)
      this._shadowRoot.getElementById('row-0').classList.add("selectedRow");
  }  

  arrowUpDown(e) {
    if(this.selectedIndex > -1)
      this._shadowRoot.getElementById('row-' + this.selectedIndex).classList.remove("selectedRow");    
    if(e.keyCode == '38' && this.selectedIndex > 0) {
      // arrow up
      this.selectedIndex --;
    }
    else if(e.keyCode == '40' && this.selectedIndex < this.filteredWordsCount - 1) {
      // arrow down
      this.selectedIndex ++;
    }
    this._shadowRoot.getElementById('row-' + this.selectedIndex).classList.add("selectedRow");
    e.preventDefault();
    return false;
  }

  rowSelected(e) {
    if(this.filteredWordsCount == 0)
      return;
    this._shadowRoot.getElementById('textfield').value = this._shadowRoot.getElementById('row-' + this.selectedIndex).innerText;
    this._shadowRoot.getElementById('drawer').innerHTML = '';
    this.filteredWordsCount = 0;
    this.selectedIndex = 0;
    e.preventDefault();
    return false;    
  }

  //business logic comes here. Bad idea!. Separate this in a diffrent class. But for the simplicity of the example we will keep it here.
  filterWords(prefix) {
    prefix = prefix.toLowerCase();
    var result = [];
    for(var i=0; i < this.dictionary.length;i ++) {
      var wordArray = this.dictionary[i].toLowerCase();
      for(var j=0; j < prefix.length && j < wordArray.length; j ++) {
        if(prefix[j] != wordArray[j]) {
          break;
        }
      }
      if(prefix.length == j) {
        var wordRow = '<p id="row-' + result.length + '">' + this.dictionary[i] + '</p>';
        result.push(wordRow);
      }
    }
    this.filteredWordsCount = result.length;
    return result;
  }  

}



window.customElements.define('textbox-with-dropdown', TextboxWithDropdown);

What we just did:
– in connectedCallback function we set up:
this.selectedIndex which will point to the selected word in the drawer
this.filteredWordsCount storing the number of word matched
this.isDrawerOpen
this._shadowRoot.innerHTML is set up with the HTML layout that we created in the beginning of the file.
this.textfield is set up with the value of value attribute. this._shadowRoot is the way how we are referring to the root of the component.
this.dictionary stores an array of all dictionary words that we pass using the dictionary property.
– added event listener to the text field, which will respond on keyUp and keyDown and will manipulate the highlighted word.
– (line 56) observedAttributes function returns a list of the attributes that we need to observe. Ie on adding or removing words through browser’s dev tools.
– (line 61) this function is called every time when the value of value attribute is called.Ie when  document.getElementById('txt1').value is called.
– (line 66) attributeChangedCallback is called whenever the observed parameters that we set up in observedAttributes function. Once the dictionary parameter is changed we are updating this.dictionary
– (line 71) selectHighlight function is called on mousemove over the words in the drawer.
– (line 77) keyUp(e) function responds to key press and is responsible to call rowSelected() on enter key, or arrowUpDown() on button up or down. If any other key is pressed filterWords(prefix)is called, prefix is the whatever the user started to type in the textfield. Then when the new words list is returned, we attach mousemove event (line 101), and click event (line 104)
arrowUpDown() simply move the highlighter up or down.
rowSelected() is called when a word is selected either with mouse click or when enter key is pressed.

 

 https://github.com/ToniNichev/WebComponents-text-box-with-dropdown.git