How to: Autocomplete Inputs in Ember

22 April 2016 ·

A form is still one the most frequently used layers between user’s and company’s goals. Almost every time when we need some input from user, we need to show him a form. And users hate forms, they don’t want to fill all these boring fields and to think a lot about what to put where and so on.

But we are front-end developers, aren’t we? We can help our valuable users!

In this Ember.js tutorial I will cover a few steps how we can help a user to fill one type of fields: the autocomplete text input field.

We will create an input field that will autocomplete tags for a blog post by providing a part of the name.

Think about this field as an input for selecting tags for your post in your blog application, or an input where a user has to select recipients of his message. An animation is worth a thousand examples:

Hopefully now you understand what I mean. If so, let’s have some fun a learn how to do this!

Getting started

I hope, you’re already a bit familiar with Ember CLI. If not, you can check their official documentation here.

First things first, we need a new app for our experiments:


ember new autocomplete-inputs
cd autocomplete-inputs

Model and Data

If you’re not familiar with models in Ember.js you can take a look a brief introduction to model here.

Now we need to prepare our model and have some data for it, so we can work with something.

Let’s create a Tag model (we will start with tags):


ember g model tag

Tag model will have only one property: name, so the model should look like this:


// app/models/tag.js
import DS from 'ember-data';

export default DS.Model.extend({
    name: DS.attr('string')
});

Our next step will be to create http-mock (a mocked version of API endpoint) for development purposes.


ember g http-mock tags

Modify the created endpoint to include some dummy data. Open server/mocks/tags.js file and find the first line that starts with tagsRouter.get and change this block to following:


// server/mocks/tags.js
...
tagsRouter.get('/', function(req, res) {
    var tags = [{
        id: 1,
        name: 'emberjs'
    }, {
        id: 2,
        name: 'webdev'
    }, {
        id: 3,
        name: 'javascript'
    }];

    res.send({
        'tags': tags
    });
});
...

Next step is to generate a default adapter and change the default namespace of API.


// application adapter
ember g adapter application


// app/adapters/application.js
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
    namespace: 'api'
});

Finally we can check if we set up everything correctly. In order to do that, let’s output the list of tags we receive from our API. For that we need to generate an application route and retrieve tags as model there:


// Generating main route
ember g route application

Define a model for the new route as follows:


// app/routes/application.js
import Ember from 'ember';

export default Ember.Route.extend({
    model() {
        return this.store.findAll('tag');
    }
});

And update a template to see if everything is working fine:


// app/templates/application.hbs
...
<ul>
    {{#each model as |tag|}}
    <li>
        <span>{{tag.name}}</span>
    </li>
    {{/each}}
</ul>

That was a long way, but after we run a command ember serve and after navigating to http://localhost:4200, we’ll see the list of our tags:

Finally, we are ready and can start developing our awesome component!

Developing a component

Let’s move on and start developing our awesome component, you’re here because of this, right?

The idea is to create a reusable component that will look in a template file like this: {{autocomplete-input items=post.tags key="name" model="tag"}}, where items will contains already selected items, key attribute will define a property that we’re looking for and finally model attribute will determine, what kind of a model we’re looking for.

So let’s generate a component:


ember g component autocomplete-input

Awesome, let’s add the component to a main template file, so we can livereload and see our changes:


// app/templates/application.hbs
...
{{autocomplete-input items=model.tags key="name" model="tag"}}

And change our route so it contains only a new property which is a container of selected tags:


// app/routes/application.js
import Ember from 'ember';

export default Ember.Route.extend({
    model() {
        return { tags: [] };
    }
});

Great, then we should move to the component and outline a template for it. First let’s define what we need:

Basic version will look like this:


// app/templates/components/autocomplete-input.js
<div class="autocomplete-input">
    {{#each items as |item|}}
    <span class="selected-item">{{item}}</span>
    {{/each}}

    {{input type="text"}}

    {{#each foundItems as |item|}}
    <span class="found-item">{{item}}</span>
    {{/each}}
</div>

What we need next? We need a search, of course, so we look for tags based on the value we typed into the input field. The search will be very basic in purpose of the tutorial.

Let’s add a key-up listener to the input in a template:


{{input type="text" key-up="keyUp"}}

Now we need to outline our component javascript file and definitely add a function keyUp():


// app/components/autocomplete-input.js
import Ember from 'ember';

export default Ember.Component.extend({
    store: Ember.inject.service(), // we will use Ember.store for search
    foundItems: [], // we need to store found items

    findItem(value) {
        // here we will send a request to API
        // to return us some items based on
        // passed value
    },

    actions: {
        keyUp(value) {
            this.findItem(value);
        }
    }
});

Let’s focus now on findItem() function:


// app/components/autocomplete-input.js
...
    findItem(value) {
        // we get `store` service to search
        // we get `model` to know which model to query
        //    and `key` to narrow down a search to one attribute
        const { store, model, key } = this.getProperties('store', 'model', 'key');

        const query = {};
        query[key] = value;

        // we query the data and set the results as ‘foundItems’
        store.query(model, query).then((items) => {
            this.set('foundItems', items);
        });
    }
...

Now we can check if everything is still working:

At the moment our mocked API endpoint doesn’t understand what we’re trying to do with this search. To fix that we need to add a basic filtering there:


// server/mocks/tags.js
    tagsRouter.get('/', function(req, res) {
        var query = req.query || null;
        var tags = [{
            id: 1,
            name: 'emberjs'
        }, {
            id: 2,
            name: 'codeschool'
        }, {
            id: 3,
            name: 'javascript'
        }];

        var filter = function(tag) {
            if (!query) { return true; }
            return tag.name.indexOf(query.name) !== -1;
        };

        res.send({
            'tags': tags.filter(filter)
        });
    });

We can try again and see, that Ember outputs everything but tag names. This is happening because we’re trying to show an internal Ember model, instead of a value. But because our key is dynamic we need to create a custom helper, very basic one, just to return a value of a passed object by a key. So in our case we need something like {{output-value item key}}:

// generate a helper
ember g helper output-value

// app/helpers/output-value.js
import Ember from 'ember';

export function outputValue(params) {
    let object = params[0],
        key = params[1];

    // just to be sure, that our helper will work as expected
    if (typeof object === 'object' && typeof key === 'string') {
        if (object.get) { // we check whether the object is Ember object
            return object.get(key);
        } else {
            return object[key];
        }
    } else {
        throw new TypeError('output-value helper signature is output-value(Object, String)');
    }
}

export default Ember.Helper.helper(outputValue);

Now let’s use this helper in our component’s template:


// app/templates/components/autocomplete-input.hbs
<div class="autocomplete-input">
    {{#each items as |item|}}
    <span class="selected-item">{{output-value item key}}</span>
    {{/each}}

    {{input type="text"}}

    {{#each foundItems as |item|}}
    <span class="found-item">{{output-value item key}}</span>
    {{/each}}
</div>

Now if we go and check our app, it should work like this:

If you see the same, then, my congratulations!

Next step is to add add() and remove() functions, so we can add our tags to the container. We may start from a template and add {{ action ... }} helper there first:


// app/templates/components/autocomplete-input.js
<div class="autocomplete-input">
    {{#each items as |item|}}
    <span class="selected-item" {{action "remove" item}}>{{output-value item key}}</span>
    {{/each}}

    {{input type="text" key-up="keyUp"}}

    {{#each foundItems as |item|}}
    <span class="found-item" {{action "add" item}}>{{output-value item key}}</span>
    {{/each}}
</div>


// app/components/autocomplete-input.js
...
    // If we have a container, we just add an item to it
    addItem(item) {
        if (this.get('items')) {
            this.get('items').addObject(item);
        }
        // after adding an item, it’s a good idea to clear results
        this.set('foundItems', []);
    },
    // If we have a container, we remove a passed item from it
    removeItem(item) {
        if (this.get('items')) {
            this.get('items').removeObject(item);
        }
    },

    actions: {
        // Remove item from the list of items
        remove(item) {
            this.removeItem(item);
        },

        // Add item to the list of items
        add(item) {
            this.addItem(item);
        },

        keyUp(value) {
        ...
        }
    }
...

Now, if you check what’s on our page, you should be able to see something like this:

Congratulations!

Voila! We have now a reusable autocomplete input, which you can use in your blog app, you finances app, any other application, that has some sort of an undefined set of defined values which we need to enter.

Just add a bit of your imagination, some CSS and you will see something like this (trust me, this is absolutely the same component):

Conclusion

Of course this is just a beginning, you’ll need to add some logic to the keyUp method to prevent an empty search, you might want to add a normal add function, that will be able to add either a model, or create a model from value and add it, and so on. The list is very long if not endless. But you’ve done a huge and great job, if you’re reading these words, so let me congratulate you one more time.

Tags:

Might also be interesting for you

Open RSS Reader

I'm starting a new open-source project "Open RSS Reader" that will be modular app so if you don't like the existing clients, you'll be able to quickly build your own. It's gonna be both open source and free to use.

SotM: pet - Awesome Command-line Snippet Manager

`pet` is a simple command-line snippet manager. It allows you to write and store snippets for the command-line, so you don't need to remember all the commands you frequently use in you terminal. It can sync your snippets to the GitHub Gist, so you can always take your snippets with you.

5 Reasons To Give Linux A Try

This time I want to share 5 reasons why I advocate for using Linux whenever I have a chance, and why you should give it a try too.

SotM: TaskWarrior - Great CLI Task Manager

Taskwarrior is an open-source, cross platform time and task management tool. It has a command-line interface rather than a graphical user interface.

Comment

comments powered by Disqus