Minimalist Ember (Part 1)

Ember’s a great framework, but like most frameworks, it can be easy to feel overwhelmed at first when you’re learning the basics. I’ve interacted with many Ember beginners in the Ember Slack community, and have noticed a consistent theme over the years; people running into problems when they sometimes try to use strange or advanced approaches to implement commonly needed features that have much simpler solutions.

One way to help alleviate this is with opinionated “guides to the guide”. These can be helpful in terms of highlighting the important bits that tend to get missed, and to put certain features in context.

Today I’m going describe a simple way to update a route’s model when user interaction requires a new view of the data. There are many ways to do this, but utilizing the humble (but powerful) query param is one of the best.

Before we get started, one quick note: if you copy paste any of the code in from the Ember twiddles into your app, you may need to change some of the statements using this.thing syntax to this.get('thing')if you are using a version of Ember lower than 3. Everything else should carry over (at least to Ember 2; I am not sure about Ember 1).

Query params are defined as properties on a route’s controller. Here is an ember-twiddle where we’ve set up a bare query-param on a route called /home. If you type some text into the search box, you will see that we already have a property in the controller bound to the URL, including a default value of '' which removes the query param from the URL if the value equals the empty string. You can also manipulate the query param in the URL directly.

import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['search'],
search: ''
});
The Ember Twiddle fake URL bar. Try changing the value of search, then press enter.

Now let’s make the query param actually do something by adding a model and filtering values. I added some data to the home route and added a computed property on the controller to filter that data. I also updated the template to show the filtered data. Here is the updated controller:

import Ember from 'ember';
import { computed } from '@ember/object';
export default Ember.Controller.extend({
queryParams: ['search'],
search: '',
  // New code
filteredSearch: computed('search', 'model.[]', function() {
return this.model.filter((hamster) => {
if(!this.search) {
return true;
}
return (hamster.name.indexOf(this.search) > -1);
});
})
});

Note that the filteredSearch computed property could be improved; it assumes that the hamster records themselves are static. model.@each.name would have been better if we ever expected hamster names to change and wanted our computed property to be recalculated whenever that happened. Our current code only checks to see if objects are added or removed to the model array.

While it’s useful to know how to write computed properties like this, many applications need to make an api call when a search like this one is performed. We’re still going to use fake data for the next example, but we’ll now set things up so the route refreshes its model whenever the query param changes. This feature is already built in, so all we have to do is add queryParams: { search: { refreshModel: true } } to our route. This tells Ember that whenever it detects that the controller’s search property has changed (either via Ember or the user manipulating the URL directly), it should trigger a full transition. And whenever a full transition is performed, the model() method gets run again, but this time with the new params. If our app were wired up to a real backend, we could use that query param to query our data source (see commented out code). Instead, we’ve removed the computed property we wrote above from the controller, and added some similar code to the model function on home’s route.js file. Here is the result.

import Ember from 'ember';
import { inject as service } from '@ember/service';
const HAMSTERS = {...}; // real code omitted for brevity
export default Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},
store: service(),
init() {
this.store.push(HAMSTERS);
},
model(params) {
    // TODO: Implement backend
// this.store.query('hamster', params)
    let hamsters = this.store.peekAll('hamster');
return hamsters.filter((hamster) => {
if(!params.search) {
return true;
}
return (hamster.name.indexOf(params.search) > -1);
});
}
});

Note that it can be a good idea in hooks like init() to call this._super(...arguments); so as to not break mixins or superclasses, should any be added later. init()is only called once in the lifecycle of the app, by the way, for singleton objects such as controllers or routes. You likely won’t need to use it much, but it’s occasionally convenient.

Just to emphasize how powerful the route’s model function is, let’s simulate a data loading delay of 1 second, and add a loading substate. Loading substates are another overlooked feature of Ember that comes out of the box, and relieves us of the chore of tracking the loading state of our models ourselves. In order to not hide the search-box when the loading template renders (we only need to show the loading template over the list of data), the home route is now broken up into a nested route setup with home and home/list routes. home/list is now responsible for loading the list of data, so it accesses the query params of the transition in its route’s model() function. home’s route is still responsible for seeding the fake data and managing the query params, but now it also automatically redirects to home/list(since it doesn’t show much by itself anymore). We can remove it’s model function, however, since home/list handles that now. This is the result. Here is a visual of the new structure:

home 
controller # same responsibilities
route # model() removed
template # search box template
   home.list   
route # provides filtered data from store based on QP's
template # name list template
-loading # loading state template

Hopefully this little guide gave you a good sense of how you update your route’s models just with query params. Ember does all of the heavy lifting with tracking changes to the url, managing loading and transitions, etc. If you omit the fake data and the model function simulating the backend’s search api, we barely wrote any code at all. Ember is full of powerful, production ready defaults like this, which are designed to keep the app simple and easy to understand, and keep you productive.