Tuesday, October 20, 2015

ember 101: steps to add a search widget component

So, usual disclaimers apply: I'm still very new to Ember (especially since the tutorial I used set things up as POD rather than the more traditional MVC my project is leaning towards) and so everything there can be taken with a grain of salt; still I thought it might be useful for me to document these steps for reference for my future self or others, and maybe run what I did by more experience Emberites.

I was starting with a code base my colleagues made, that had a "campaigns" route and template. I used ember-cli to generate the basic parts:

$ ember g component campaign-search

Then I filled in the basic template in campaign-search.hbs: an input widget and a button to trigger the "search" action:

{{input placeholder="search" value=searchterms}}
<button {{action 'search'}}>search</button>

The campaign-search.js was pretty trivial:

import Ember from 'ember';

export default Ember.Component.extend({
actions:{
search(){
var searchTerms = this.get("searchterms");
this.sendAction("search",searchTerms);
}
}
});

So here I'll take a second to point out I'm glossing over some thinking I had to do about how to structure this component relative to its parent (campiagns.hbs/capmaigns.js which is holding the model that the search would need to adjust.) The search component didn't modify the model directly; instead it did a "sendAction" to the parent. It's obvious in retrospect, but this guy's sole contracted role is to simply send these search messages, it doesn't care about the underlying actions that must happen next to update the model.

I then placed a reference to the widget in the parent template campaigns.hbs...

{{campaign-search search="search"}}

Now what might be confusing is this... the search on the left of search="search" is the name of the action to use, and the "search" it's set to refers to the action defined in campaigns.js. The convention seems to be to use the same term at various levels, though search="search" requires a lot of mental scoping... (especially since "search" is also the action the button triggers!) At one point, I called the campaigns.js action (that actually did the work) updateSearch, and I told the main search action to call .sendAction("widgetsearch"), and so at that point in development the tag was

{{campaign-search widgetsearch="updateSearch"}}

which made it clearer to me, that we were linking a sendable "widgetsearch" key to be bubbled up to "updateSearch" on the parent template/route. (Still not sure I dig the naming convention, but we'll see)

Anyway, the other mental breakthrough I needed was: Ember really embraces the URL driving the current state.  Therefore, the campaign.js route shouldn't modify the current model directly, but rather change the URL which in turn will adjust the model to the correct thing. So search() as defined in campaigns.js can simply be
  search(searchTerms) {
this.transitionTo({queryParams: {search: searchTerms}});
  }
I then had to tell the route about its parameter, so the top became:
import Ember from 'ember';

export default Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},  //[...]
This said that when the search parameter got updated, it was time to refresh the model.

The model function (we're experimenting with using the new ES6 syntax, so if you don't know the first line below is equivalent to model: function(params){   ) was as follows before I messed with it:
model(params) {
  return Ember.RSVP.hash({
    camps: this.store.findAll('campaign')
   });
}

The "search aware version" of that is
model(params) {
  if(! params.search){
return Ember.RSVP.hash({
camps: this.store.findAll('campaign')
});
  } else {
return Ember.RSVP.hash({
camps: this.store.query('campaign',{q : params.search})
});
  }
},

With that done I wanted to update mirage (the part doing or RESTful mockup) so that /api/campaigns?q= would work. This is just a quick dirty filter that, when there's a "q" parameter, filters through all campaigns and only returns ones where a conglomerated value of the searchable fields matches the search term...
    this.get('campaigns', function(db,req) {
        var allCampaigns = db.campaigns;
        if(! req.queryParams.q) {
          return { campaigns : allCampaigns };
        }
        else {
          var myFilter = req.queryParams.q.toLowerCase();
          return { campaigns : allCampaigns.filter(function(campaign){
            var searchable = campaign.name.toLowerCase() + " " + campaign.id;
            return searchable.indexOf(myFilter) !== -1;
          })
         };
        }
    });
So besides writing testing, there was just one more problem: when you entered a search term, the input box was cleared out. For that it seemed expedient to add the "current search terms" as part of the model, so that part of the model function above became 
return Ember.RSVP.hash({
camps: this.store.query('campaign',{q : params.search}),
searchterms:params.search
});

and in the template, I had to pass it to component:
{{campaign-search search="search" searchterms=model.searchterms}}

So there are parts of this that still seem like wonky black magic to me... the way the search action that the button in the widget calls is distinct from the "search" (linked to the parents function) that bubbles up when it calls sendAction(), and the automagic way setting a value where the component is embedded in the parent template "Does What I Mean" in filling the appropriately named input field. Still, as I get more fluent in Ember, this stuff will feel less odd to me, I'm sure.

No comments:

Post a Comment