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.

Leave a Reply

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