Ember.js ajax autocomplete

Purpose of this page is to show my solution for a basic ajax crafted autocomplete service. Since I ran into 1001 problems, I assume this may help others, too.

Especially if you ran into this error message:

Uncaught Error: Assertion Failed: The value that #each loops over must be an Array.

The reason was quite obvious. I didn’t have an array, what I got was the one object that find() returns. You will see below.

I found these videos helpful, to get a better feeling for how Ember.js works under the hood:

I will not explain more than necessary to the code. Please refer to the videos for introductory advices.

This is my assumed ajax service output:

{"findwords": [
  {
    "id": 1,
    "simplified": "你好",
    "traditional": "你好",
    "pinyin": "ni3hao3",
    "english": "Hallo"
  },
  {
    "id": 2,
    "simplified": "对",
    "traditional": "對",
    "pinyin": "dui4",
    "english": "Correct"
  }
]}

Notes:

  • The JSON output must be fully properly quoted. If your promise gets rejected, you might have a JSON syntax issue (or a structure issue).
  • The model identifier (here „findwords“) must be the result object property that holds the actual result.
  • The example makes use of ember-data.

This is the HTML template file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Autocomplete Example</title>
  <link rel="stylesheet" href="css/libs/bootstrap.min.css">
  <link rel="stylesheet" href="css/libs/bootstrap-theme.min.css">
  <link rel="stylesheet" href="css/style.css">
  <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1" />
  <script src="js/libs/jquery-1.10.2.js"></script>
  <script src="js/libs/handlebars-1.1.2.js"></script>
  <script src="js/libs/ember-1.5.1.js"></script>
  <script src="js/libs/ember-data.js"></script>
  <script src="js/libs/bootstrap.min.js"></script>
  <script>
</head>
<body>
  <script type="text/x-handlebars">
        {{outlet}}
  </script>

  <script type="text/x-handlebars" id="autocomplete">
    {{input type="text" value=phrase placeholder="Add words to your drill"}}
    <ul>
      {{#each searchResults}}
        <li>{{this.pinyin}} - {{this.english}}</li>
      {{/each}}
    </ul>
  </script>

  <script type="text/x-handlebars" id="form">
    {{render "autocomplete"}}
  </script>
</body>
</html>

The input field is connected to the „phrase“ attribute of the controller.


App = Ember.Application.create();

// Model
App.Findword = App.Findwords = DS.Model.extend({
  simplified: DS.attr(),
  traditional: DS.attr(),
  pinyin: DS.attr(),
  english: DS.attr()
});

// Routes
App.Router.map(function() {
  this.route("form", { path: "/" });
  //this.route("findwords", { path: "findwords/:phrase" });
  this.route("findwords", { path: "findwords" });
});

The application needs to know the data structure of the JSON objects – the model for it.
I created „Findword“ and „Findwords“ because I found Ember.js requesting both, even though only „Findwords“ is specified
I first assumed it’s a good way to have the „:phrase“ attached to the path, but found out later that when searching … URL parameters are more convenient.


// The controller
App.AutocompleteController = Ember.ArrayController.extend({
  phrase: '',
  searchResults: function() {
    var searchText = this.get('phrase');
    if(searchText.length > 0) {
      return this.store.find("findwords", {"phrase": searchText});
    }
  }.property("phrase")
});

The controller has a property „searchResult“, which you find above in the HTML file. It provides the search result.
As soon as something is entered in the input field, the method loadData is fired, filling the search result.

Notes:

  • find() makes an ajax call but only returns one item from the list – if an id is specified.
  • findAll() does not accept parameters
  • filter() does not make an ajax call
  • Returning the promise of a find(type,id) call directly, results in an error telling #each cannot iterate over a Promise, but expects an array.

find() is the way to go, but only with parameters in form of an object.

That’s basically it.

I had some trouble debugging the „#each/Promise“ issue and landed at an also working solution. It indirectly fills the „searchResult“ with objects from the promise. That could maybe serve in other cases, so here it comes:


// The controller
App.AutocompleteController = Ember.ArrayController.extend({
  phrase: '',
  loadData: function() {
    var searchText = this.get("phrase");
    var controller = this;
    if(searchText.length > 0) {
    this.set("searchResults", Ember.A() );
    this.store.find("findwords", {"phrase": searchText}).then(
      function(data) {
        controller.get('searchResults').addObjects( data );
      },
      function(data) {
        alert('rejected, probably syntax error!');
      }
    );
    }
  }.observes("phrase"),
  searchResults: Ember.A()
});

Add Comment

Required fields are marked *. Your email address will not be published.