Skip to content

Instantly share code, notes, and snippets.

@machty
Last active December 31, 2015 02:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save machty/7923797 to your computer and use it in GitHub Desktop.
Save machty/7923797 to your computer and use it in GitHub Desktop.
Ember query param thought

Ember Query Params

This is the latest in API thought. Is it any good?

New/Updated API Surface

Ember.Controller

ThingController = Ember.Controller.extend({

  foo: "myDefaultValue",
  bar: ['such', 'api'],

  complicatedObj: { complex: 'thing' },

  maverick: "imsoglobal",
  
  // queryParams if all defaults:
  // queryParams: ['foo', 'bar', 'maverick']

  queryParams: {
    // Register `foo` as a queryParam.
    // Once the route associated with this controller
    // has entered, `foo` will be bound to
    // "thing[foo]=___" in the URL. If no one
    // sets foo to some other value, the URL
    // will have "thing[foo]=myDefaultValue".
    foo: true,

    // Register `bar` as a queryParam.
    // Its default value is an array of strings,
    // so it will be serializable in the URL as
    // "thing[foo][]=such&thing[foo][]=api"
    bar: true,

    // complicatedObj: true...
    // can't just enable defaults on complicatedObj;
    // need (de)serializers.
    // If (de)serializers weren't provided, helpful
    // errors would throw.
    complicatedObj: {
      deserialize: function(urlStringValue) {
        return { complicatedObj: "someString" + urlStringValue };
      },
      serialize: function(obj) {
        // If complicatedObj were set to obj,
        // this would cause the URL query param
        // for complicatedObj to be "thing[complicatedObj]=omglol"
        return "omglol";
      }
    },
    
    // maverick overrides the key used in generating the URL
    // query param, e.g. "maverick=imsoglobal" vs
    // the default "thing[maverick]=imsoglobal".
    // Note: this is not a solution for having multiple
    // controllers binding to the same query params, this
    // is just a way to letting you override the formatting
    // default. Global query params should be on application
    // route and other controllers can bind to the 
    // property on application controller.
    maverick: {
      key: 'maverick'
    }
  }
});

Alternatives to the above API exhibited extreme weakness when it came to serializing/deserializing, or providing default values for query params upon entering routes without awkwardly repeating this default at the route layer.

Ember.Route

Ember.Route.extend({

  // Schedules a "refresh" transition where by
  // the route hierarchy doesn't change but 
  // all the model hooks are re-run, starting
  // from this route. If a parent route calls 
  // refresh() immediately after this one does, 
  // the parent route will be the starting
  // point for the refresh().
  refresh: function() {},

  actions: {
    queryParamsDidChange: function(changedQueryParams) {

      // A `queryParamsDidChange` action fires when
      // the URL updates or a transition has occurred
      // that updates query params. At the beginning
      // of a link-to, transitionTo, or handleURL, 
      // if a change in query params is detected,
      // a queryParamsDidChange will be fired from
      // the pivot route upward, giving routes above
      // the pivot route a chance to request a refresh(),
      // which will cause them to be the pivot route for
      // this transition. They could also transitionTo
      // elsewhere at this point (though don't have
      // a solid use case in mind for that).

      // This is the default implementation of 
      // this action handler provided for each route.
      // Conceivably you could override the handler
      // and perform a more thorough/complicated
      // check as to whether a refresh transition
      // should occur.

      var refreshForQueryParams = this.get('refreshForQueryParams');
      refreshForQueryParams.forEach(function(qp) {
        if (changedQueryParams.hasOwnProperty(qp)) {
          // Schedule a refresh.
          this.refresh();
        }

        // Bubble, so that parent refresh()'s override
        // this one and cause the refresh to start
        // at a higher point.
        return true;
      });
    }
  },

  // This is the quickest/easiest way to set this route
  // up to self-refresh when its query params change.
  // If these weren't specified, changes to the query
  // param wouldn't cause a transition/refresh at all
  // and the effects of a changing QP would only
  // impact the controller.
  refreshForQueryParams: ['foo', /^bar/]
});

link-to

{{#link-to 'dest' foo=123}}

The above generates "/dest?dest[foo]=123" assuming that the dest property has been configured to be a QP property on DestController.

{{#link-to 'dest' parent:bar=123}}

If dest route has a parent route called 'parent', and ParentController#bar is a QP property, then this will generate "/dest?parent[bar]=123".

link-to is smart enough to look at the destination route hierarchy and know what query params have been set up on the controllers associated with that route hierarchy (we're not supporting anything super fancy like {{render}} though).

Problem

The charset of a handlebars helper hash key is pretty restrictive; we wouldn't even be able to handle setting the prop on a controller whose module name had a dash in it.

Maybe this means we just get rid of the colon approach and enforce the rule that controllers in the same hierarchy can't reuse QP property names.

So the parent example would just become:

{{#link-to 'dest' bar=123}}

and generate the same url: "/dest?parent[bar]=123"

Updated Transition Flow

It's all in my brain and largely solidified, but will need to write it out at a later point because I am TIRED.

Here are a bunch of thoughts though:

  • Default behavior: updating a query param causes a replaceState by default. This should probably be configurable somehow. How?
  • The queryParamsDidChange action is how this no-transition-on-qp-changes behavior is overridden; if a route handles this action and calls this.refresh(), that causes what would have just been a controller property update to turn into a full on classic router transition whereby model hooks and the like are called.
  • The actually controller setting of QP properties always happens at the end of the transition process, rather than in setupController (though you'll have the ability to more eagerly set these properties in setupController). The reason for this is that there are many different ways to update QPs (url changes, transitionTos, link-tos, property changes), and not all of these ways involve transitions, and it just seems like race condition hell to, for certain cases, set these properties in a setupController hook and in other cases set the properties elsewhere. So we're gonna set all of the properties all at once, at the end of a transition.

Punting on

Promises. For v1 at least.

@Romanior
Copy link

Sorry to interrupt your mindflow, but these type of URLs "/dest?dest[foo]=123', '"/dest?parent[bar]=123" are so
against common practices and current API's support.

I'm sure, you understand that everyone uses, first of all, simpler URL structure, including github.
https://github.com/emberjs/ember.js/issues?labels=assert&page=2&state=open

And those type of URL should be supported by default.
I'm sure its even not my opinion only, if you would you be willing to ask, but may be I'm missing something...

Best regards, Roman.

@tylr
Copy link

tylr commented Dec 14, 2013

I have to +1 @Romanior's statement regarding complex query params. This is not a URL structure I'd introduce to an app.

I experimented with the existing implementation this week.

Are you planning on making query params globally/sticky like the current implementation? I can not imagine a scenario where you'd want to retain query params when transitioning between routes or resources.

Managing {{link-to}} gets pretty ugly too. You're essentially writing logic into {{link-to}} by taking into consideration all the possible query params states and setting or unsetting them. Even worse, with sticky/global params you are forced to unset params for unrelated resources, coupling your code in a terrible way.

I'm also concerned tacking this on top of {{link-to}}. The charset limits are one thing, but the collision likelihood is also high. For example in @Romanior's URL comment, state=open would be a link-to property collision and break.

Currently, I use a filters property on controllers when I need to support filtering/sorting/paginating interactions. I end up writing logic in my controllers to manage the filters.With the current implementation this code was cleaned up substantially.

Last, lack of query param support has become a UX pain point. I'm confident that a considerable amount of query-param usage will be for pagination, sorting. I'd be pleased with a narrow scope implementation that just let me do those things.

@Romanior
Copy link

@tylr One case I would imagine, where you'd want to use a "stickiness" of QP, can be
I have a catalogue with multiple categories, but I want to keep filtering state of the list between the transitions from the category to category. e.g. if I filtered by "NIKE" it shoes, when clicking at t-shirt, is very likely that user wants to see nikee t-shirts.
Agree with you with the rest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment