Friday, September 20, 2019

Building Input autocomplete component using Ember

Ember is a framework for ambitious web developers, even though building application using Ember is faster, there were some bottlenecks like creating a select list using HTML and getting the data from the HTML element and submitting to the API etc.

Ember comes with a power-train called components and this will solve our problem, we should architect components carefully so that can be reused across. The solution is through mix of things which i experienced and there might be better way of solving the same problem.


Assumptions 

Latest version of Ember is installed and you have basic experience of building a Application with knowledge of using HTML, Javascript, NodeJS, Ember Rest Adapter, Ember Model, Ember View and Ember Controller.

Step 1

Run the following command under Ember project folder


ember g component input-autocomplete  
 
The above command will create a controller and view for the component. The controller will be created under component folder with name input-autocomplete.js and template with name input-autocomplete under template/component/input-autocomplete.hbs.
The following code should be copied into the component template.
{{input
  name=this.name
  value=this.value
  placeholder=this.placeholder
  key-up=(action "search")
  focusIn=(action "showdropdown")
  focusOut=(action "hidedropdown")
  class="form-control"
}}
<div id="myInputautocomplete-list" class="autocomplete-items">
    {{#each this.filteredResult as |data|}}
        <div {{action 'choose' data.title}} {{action "choose" data.title on="mouseDown"}} >
            {{data.title}}
            <input type="hidden" value="{{data.title}}">
        </div>
    {{/each}}
</div>

Step 2

Edit your component controller file and add the following code, in our case it is component/input-autocomplete.js
import Component from '@ember/component';


export default Component.extend({
    value: '',
    currentFocus: -1,
    childCount: 0,
    filteredResult: null,
    lov: null,
    init() {
        this._super(...arguments);
        this.filter('').then((results=> this.set('filteredResult'results));
    },
    didRender() {
        this._super(...arguments);
        //this.currentFocus = -1;
        this.lov = this.element.children[1];
        if (document.activeElement !== this.element.firstChild) {
            this.element.children[1].style.display = "none";
        }
        else 
            if (this.childCount !== this.element.children[1].children.length) {
                this.currentFocus = -1;
                this.childCount = this.element.children[1].children.length;
            }
    },
    keyDown: function (evt) {
        var charCode = (evt.which != undefined? evt.which : event.keyCode;
        if (charCode === 40) { //Up Arrow Key
            if (this.currentFocus < this.element.children[1].children.length-1)
                this.currentFocus++;
            else 
                this.currentFocus = 0;
            this.removeHighlight();
            this.highlight(this.currentFocus);
        } else if (charCode === 38) { //Down Arrow key
            if (this.currentFocus > 0)
                this.currentFocus--;
            else 
                this.currentFocus = this.element.children[1].children.length-1;
            this.removeHighlight();
            this.highlight(this.currentFocus);
        } else if (charCode == 13) { //Entter Key
            this.set('value',this.element.children[1].children[this.currentFocus].children[0].value);
            this.removeHighlight();
            this.element.children[1].style.display = "none";
            this.element.children[0].blur();
        } 
    },
    highlight: function(pIndex) {
        this.element.children[1].children[pIndex].classList.add("autocomplete-active");
    },
    removeHighlight: function() {
        for (var i =0 ; i < this.element.children[1].children.length ; i++) {
            this.element.children[1].children[i].classList.remove("autocomplete-active");
        }
    },
    actions: {
        choose(pSelectedText) {
            this.set('value',pSelectedText);
            this.removeHighlight();
            this.element.children[1].style.display= "none";
            this.element.children[0].blur();
        },
        hidedropdown() {
            this.element.children[1].style.display = "none";
        },
        showdropdown() {
            this.element.children[1].style.display = "block";
        },
        search() {
            this.element.children[1].style.display = "block";
            let filterInputValue = this.value;
            let filterAction = this.filter;
            filterAction(filterInputValue).then((filterResults=> this.set('filteredResult'filterResults));
        }
    }
});

Step 3
Edit your parent controller file and copy the following code, in my case it is controller/todo.js


import Controller from '@ember/controller';
export default Controller.extend({
    filter:'',
    filteredList: null,
    actions: {
        searchTodo(param) {
            if (param !== '') {
              return this.store.query('todo', { filter: {title: param } });
            } else {
              return this.store.findAll('todo');
            }
        }
    }


});

Step 4

  Edit your parent view file and copy the following code, in my case it is templates/todo.hbs


    <div class="form-group">
        <label for="title">myTodo</label>
        {{input-autocomplete value=mytodo name="mytodo" placeholder="Enter Todo" id="mytodo" filter=(action 'searchTodo')}}
    </div>

Step 5

Start your Ember server with the following command




ember server
 
Step 6 

Run the application from web browser by entering http://localhost:4200/todo focus on the autocmplete field which we created and you will notice a list of todo's appearing from the database and when start typing will notice the list getting filtered, you can use up arrow, down arrow and mouse to navigate in the autocomplete select list.

Note: I am using a Rest based Adapter pointing to nodeJS API with mongoDB. Following code is part of adapter/application.js


import DS from 'ember-data';


export default DS.RESTAdapter.extend({
    host: 'http://localhost:3000'
});

Todo Model as Bonus code


import DS from 'ember-data';
const { Model } = DS;
import { computed } from '@ember/object';


export default Model.extend({
    title: DS.attr('string'),
    body: DS.attr('string'),
    date: DS.attr('date'),
    created_at: DS.attr('string', {
        defaultValue: function() {
            return new Date();
        }
    }),
    isexpired: computed('date',function() {
        return new Date() > this.get('date');
    })
});

Serializer as Bonus code


import DS from 'ember-data';
import { assign } from '@ember/polyfills';


export default DS.RESTSerializer.extend({
    primaryKey: '_id',
    serializeIntoHash: function(hashtyperecordoptions) {
        assign(hashthis.serialize(recordoptions));
      }
});
  

Hope the code is pretty straight forward and hope it helps you. 

Happy Embering :).