Ember: Using queryParams to distinguish “editing a record” from “making a new record” on a route

This guide uses Ember 1.13

ember_logo

The concept is simple and familiar: you have a page (“route”) in your Ember app that you want to use to either enter new data or modify existing data on the same record.

Example: your user is presented with a blank form to enter their shipping address.  The user enters their info and submits it (thus creating a new record). When your user goes to edit their saved shipping address, that same form is populated with their existing shipping information (loading a record). The user edits, saves, and the updated record is persisted.

One form, two behaviors (new and edit). 

This, like many things in Ember, was surprisingly painful to get working. I found queryParams to be a good solution, but the documentation on queryParams was difficult for this beginner to understand and it contains (what I think is) at least one mistake that held me up for a long time while I figured it out.

It didn’t take much code to get this working, just a lot of guessing and trying, so I thought I’d add my knowledge to the (vast, mostly out-of-date now that it’s 2016) library of Ember queryParms knowledge out there on the internet. Hopefully this helps someone.

About this tutorial

In this guide, I’m going to show you how I used queryParams in Ember to distinguish the “new” state from the “edit” state on the same route (“basics”).

About the example app

This guide uses my Ember app, “Classis”, for many of its examples, which you can explore in full on Github (it’s a work in progress but the queryParams part is done on the “basics” route if you want to look at the app that inspired this post).

“Classis” is a (fake) web app where people sell “seats” in classes they teach: tutoring, dog training, etc. It’s a lot like listing a room on AirBnB, dog boarding on Rover.com, etc. Think of classes as products people are selling, and you’ve got the idea. I call refer to “classes” as “products” in this tutorial to cut down on confusion with the programming concept, but they are called classes in my app. 

About queryParams in the context of editing records

You can use queryParams for lots of things. This particular guide shows you how to use them for editing records that have ids.

If the user is trying to edit an existing record, we can get its id and pass it along to the route where the user will edit that record. If the route sees you gave it the id, it will behave differently.

The “I’m making a new one” url looks like this:

http://localhost:4200/basics

The “hey, I’m editing a specific one” url looks like this:

http://localhost:4200/basics?id=1

Pass the queryParam from somewhere

There are lots of ways to pass a queryParam around, but in my case, I needed to pass it from an “Edit” button contained within a “class-card” component. That component’s job is to display a user’s completed product, and one “class-card” is displayed for each product the user has. Product data is passed into a class-card, and that product data includes the id.

Basically, I have a product with an id. The component knows about the product’s data, including its id. I want to pass that id to the basics page where I will edit the basic info about a product.

(I simplified the styling classes in this example and bolded the relevant part)

components/class-card.hbs

{{#link-to 'basics' (query-params id=class.id) class="mdl-button"}}
  EDIT
{{/link-to}}

Now the class id (product id) will get sent to the ‘basics’ route.

You can also pass queryParams on a transition in, say, an action. Here’s a hypothetical action method I made just for this post, it’s not in my app anywhere but I included it here in case you need to pass a queryParam from an action:

goToBasics() {
  const route = this;
  const id = this.controller.get('id') || '';
  route.replaceWith('basics', {queryParams: {id: id}});
 },

Define the queryParam on the “parent” controller

There are many ways to structure an Ember app, and I can’t claim to know them all. I do know, however, that my app uses application.js as its parent controller, so this is where I defined queryParams.

That definition looks like:

application.js

import Ember from 'ember';

export default Ember.Controller.extend({
 queryParams: ['id']
});

On a complex app, you might bury this deeper down inside the app, so that (perhaps) user ids don’t get confused with product ids or whatever, but on this simple app, the parent-most parent can handle it.

Add the queryParam to the route in router.js

All of my app’s routes are defined in app/router.js.  ‘basics’ is the route that you can pass an id to to distinguish “new” from “editing”.

Contrast with location/:id, which treats id as a dynamic segment (link goes to Ember documentation on the subject).

I included this here to illustrate the difference, because it’s easy to confuse the two (like I did when I was getting started with this stuff).

app/router.js

Router.map(function() {
  this.route('basics', {queryParams: 'id'}); //queryParam
  this.route('location', {path: 'location/:id'}); //dynamic segment
  this.route('main');
});

Url with a query param looks like:

http://localhost:4200/basics?id=1

Url with a dynamic id segment looks like:

http://localhost:4200/location/45

Long explanation of why I did it this way:

In my app, the user goes from basics to location, in that order. Basics is the first page the user encounters. It can be used to either create a new record or edit an existing. Location, by contrast, is always used to edit a record that exists, because that record was created by basics, the page before it. If you’re on /location, it’s because you are making changes to a record that has already been created, even if you’re entering location data for the first time.

In Ember, having some understanding of what order pages are encountered in can help you make these sorts of architectural decisions (at least, until your designer decides location should come first, then you get to change this stuff or maybe handle everything with a queryParam in the first place, or whatever makes sense for your app).

Add the queryParam to the route itself

Basics is where the user edits “name” and “description” of an individual product.

Inside Route.extend, define the queryParms object like so:

routes/basics.js

export default Ember.Route.extend({
  queryParams: {
    id: {refreshModel: true}
  },

Now /basics knows that it might sometimes get passed an id, and if it does, it should refresh its model.

Get the queryParams inside the model

!!This part was hard to figure out!!

Ember’s documentation just shows one parameter passed into model, but try as I might, param just came in as a useless empty { } object. I could see my queryParam in the url, but I couldn’t get at it from param.

After much Googling, I found the answer: the queryParams are on the second parameter accessed by model, which you can name transition:

(still in) routes/basics.js (right below the queryParams object you just made)

//queryParams are contained inside transition, which has to be passed as    the 2nd parameter to model

  model(param, transition) {
    let id = transition.queryParams.id;
    if (id) {
      return this.store.peekRecord('class', id);
    }
  },

IT WORKS!

I’m not sure if it’s my Ember newbieness or an actual oversight in the 1.13 documentation, but model(…) {…} totally takes more than one parameter and the second one contains that sweet, sweet queryParam you passed in.

The code in my example above looks at transition, sees if it contains an id, and if it has an id, it gets the matching ‘class’ record out of the store.

A lot of Ember tutorials perpetuate the idea that if you do model(param), the queryParam will be accessible on it, so maybe this worked at one point in Ember’s history or I just set it up wrong in mine.

Curiously enough, the guide to model hooks that enlightened me to the transition parameter also says there’s a third parameter for model straight up called “queryParams”(!!!), but in my case, that third parameter comes in empty. Only the second parameter (transition) had anything useful. I cannot explain this, I’m just reporting it in case it’s useful to someone else. (Maybe someday I will understand all of Ember’s mysteries.)

Load existing record data in setupController (but only if it exists)

Almost there: let’s load that model data into the controller now that we have it.

(still in) routes/basics.js (below the model method) 

setupController(controller, model) {
  let application = this.controllerFor('application');
  application.set('pageTitle', 'Basics');

  if (model) {
    //we're editing an existing class
    this.controller.set('existingClass', model);
    this.controller.set('editingExisting', true);
    this.controller.set('title', model.get('title'));
    this.controller.set('description', model.get('description'));
  } else {
    //this is a new class
    this.controller.set('title', '');
    this.controller.set('description', '');
  }

Now, when we visit /basics with a queryParam in our URL, we’ll see our product’s title and description! If we visit /basics without a queryParam, title and description will be empty.

We are almost done…

Update the existing Ember record if we’re editing an existing one, create a new record if it’s a new one

Finally, we need to tell our route what to do when continuing past this page. If we have a queryParam, we want to update that record and move on to /location. If we don’t have a queryParam, we want to make a new record and move on to /location.

routes/basics.js (in the action hash now) 

actions: {

  transitionToNext(newClass) {
    console.log("going to location");
    this.replaceWith('location', newClass);
  },

  failure() {
    console.log("failed!");
  },

  continue() {

    //editing an existing class
    if (this.controller.get('editingExisting')) {
      let existingClass = this.controller.get('existingClass');
      existingClass.set('title', this.controller.get('title'));
      existingClass.set('description', this.controller.get('description'));

      existingClass.save().then(() => {
        //go to location and pass along the this existing class's id
        this.replaceWith('location', existingClass.get('id'));
      }).catch(this.failure);

  } else {

    //making a new class
    var newClass = this.store.createRecord('class', {
      title: this.controller.get('title'),
      description: this.controller.get('description')
    });

    newClass.save().then(() => {
      //pass along the newly created class's id
      this.replaceWith('location', newClass.get('id'));
     }).catch(this.failure);
    }
  }
}

It’s the biggest block of code in this guide but it’s straightforward: if you have the editingExisting flag set in the setupController, set the updates and move onto location. If you don’t have this flag, make a new record.

Debugging advice

If you get stuck, remember you can look in the Ember Inspector to see if your records actually exist and confirm that they have the IDs you expect.

ember-data-record

You can also console log the id at various points in your code to see if it’s coming where you expect it to.

You can also display the id in the template, which I found helpful in confirming that what I was seeing was indeed, what I was getting, like so:

ember-display-class-id

I click EDIT and that same ID is now in the url:

ember-queryParams-url-example

That’s it!

This post was inspired by my work on a personal project I keep publicly available here on github – check it out if you’re new to Ember and want to see a relatively simple Ember app demonstrating queryParams and other concepts.

Ember: Making a button component that calls an action

This guide uses Ember version 1.13.

ember_logo

I recently started a new Ember 1.13 app to explore Google’s Material Design Lite style library. I use Ember in my day job, too, and something that has always stuck out to me as “should be easy but isn’t” in Ember is making a button component that calls an action. 

I wanted a “continue button” component that…

  • can be re-used throughout my app
  • calls an action customized per route (ie: the continue button on the ‘basics’ route goes to the ‘location’ route, the continue button on the ‘location’ route goes to the ‘students’ route, etc)

This was surprisingly unintuitive in Ember. I expected to pass an action into the component from the parent template and have the component know where to find that action.

What not to do

I think many people who try to put an action on a component start with something like this. Note that this approach doesn’t actually work:

badExampleRoute.hbs

{{bad-example-button action=”wontWork”}}

badExampleRoute.js

actions: {
  wontWork() {
    console.log("you'll never see this!");
  }
}

What’s missing? The call to the route’s action has to come from the component itself.

The rest of this guide will show you what does work for getting a component to fire an action on the parent’s route in Ember.

Generating the button component

Do this step inside your Ember project directory (assuming you are using ember-cli):

ember generate component continue-button

In the route’s template

First, add the component into your .hbs template. The action it calls, “continue”, needs to be defined in the action hash in this route’s .js file.

basics.hbs

{{continue-button actionToCall="continue"}}

In the route’s js file

In your route’s .js file, create an action hash if you don’t have one already (actions: {…}) and put a “continue” method inside it. I like to stick a console log in here, too, on the first try so I have something to look for in Chrome to confirm it’s working.

basics.js

actions: {
  continue() {
    console.log("continuing on to location page");
    this.replaceWith('location');
  }
}

Code for the button component

When you generated the button component you got an .hbs template file and a .js file.

continue-button.hbs

The important piece here is that the <button> tag contains {{action “doButtonThing”}}.

<button {{action "doButtonThing"}} class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent">Continue</button>

continue-button.js

doButtonThing is an action on the component’s own action hash. When you call doButtonThing, it’s going to fire a sendAction that references the actionToCall defined up on the component in the route template.

import Ember from 'ember';

export default Ember.Component.extend({
  actions: {
    doButtonThing() {
      this.sendAction('actionToCall');
    }
  }
});

Yes, this is confusing as #$*! and I’ve not seen a better way to do this in my year or so of developing in Ember. It feels very much like a child (the component’s .js) is telling a parent (the route’s .js) what to do.

Another way to think of it

The {{continue-button …}} up in basics.hbs knows what to do when clicked (call “continue” action in basics.js) but it’s not going to do it until told to do it. It sits around listening for the “fire!” command.

When the user clicks the button, the code on the button component itself has its own action to call. That action yells “fire!” to the instance of the button waiting up there on the route. The {{continue-button …}} up in basics.hbs hears the fire command, delivered from the component itself in the form of a send-action.

There is no “passing” of an action into the component, it’s more like a pairing of broadcaster and listener. 

The Ember community calls the larger concept at work here “data down, actions up” which took me a while to internalize. You can read more about DDAU here.