ember.js: setting a controller property in `setupController` does not update the query param

if you have a Controller defined as

App.IndexController = Ember.Controller.extend({
  queryParams: ['foo'],
  foo: ''
});

and in the Routes setupController you do something like…

setupController: function(controller, model){
  controller.set('model', model);
  controller.set('foo', 'bar'); // <-- setting this should update the query param
}

The query param does not get set in the url.

You can see a working bin here: http://emberjs.jsbin.com/dekuj/1/edit

About this issue

  • Original URL
  • State: closed
  • Created 10 years ago
  • Comments: 29 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Wait so this issue got closed with an alternate solution to work around the bug it reports, and that solution involves setting a property in a computed getter?

I too am seeing this issue on Ember 2.11, trying to clear a query param in the controller has no effect, @barelyknown 's workaround gets me most of the way there but I would still consider this an open bug.

EDIT: Ignore all this jazz. There’s some bad/wrong information here, and @locks pointed me towards a much better solution—with a query parameter instead of a dynamic segment—in-line immediately below. It binds the query parameter to a normal property with a shadow computed property that sanitizes and sets on get (my previous attempts involved binding the query parameter to a computed property that sanitized and set on set). Sorry for the noise!

The better solution...
// app/router.js
Router.map(function() {
  this.route('shop');
});

// app/routes/shop.js
export default Ember.Route.extend({
  queryParams: {
    _selectedItemIndex: {
      replace: true
    }
  }
});

// app/controllers/shop.js
import computed from 'ember-macro-helpers/computed';

export default Ember.Controller.extend({
  cart: Ember.service.inject(),

  queryParams: {
    _selectedItemIndex: 'i'
  },

  _selectedItemIndex: 0,

  selectedItemIndex: computed('_selectedItemIndex', {
    get(index) {
      const itemsLength = this.get('cart.items.length');

      if (isNaN(index) || index < 0 || index >= itemsLength) {
        index = 0;
      }

      return this.set('_selectedItemIndex', index);
    },

    set(index) {
      return this.set('_selectedItemIndex', index);
    }
  })
});
The original problem and solution...

I lost the better part of yesterday to this issue that still persists in 2.14-beta. My use case is sanitizing a query parameter to use as an index into an Ember.Array. If it’s malformed (e.g. NaN or a string) and/or out of bounds, it should use a default index, and update the URL to reflect the adjustment (so that events in response to future clicks trigger appropriately—I can explain this further to help make the case that this is not merely a cosmetic issue).

I can’t set the property from setupController() as it does not update the URL (as described in this issue). I can’t set it in the next tick using Ember.run because that will take effect too late (i.e. an invalid index would have already been used to access the array). I can’t set it once and then set it again in the next tick to update the URL because there is no property change (or at least that’s what I suspect is the reason why it doesn’t work).

I can’t use a computed property with a sanitizing setter in the controller because you can’t bind a query parameter to a computed property. I can’t use an observer on a normal property because you can’t set the property you’re observing. I can’t use an observer with Ember.run due to the issue described above.

I can’t call this.transitionTo({ queryParams: .. }) from setupController() or the beforeModel() hook or controller.transitionToRoute({ queryParams: .. }) from setupController() due to #14606 and related issues. I can’t set refreshModel to true for the query parameter because other stuff is happening in the model() hook that should not be repeated if this query parameter changes.

Clearly there is a nexus of pain here. This issue, issue #14606, and the limitation of not being able to bind a query parameter to a computed property all combine to create a neat, frustrating little trap.

Here’s my workaround: I created a nested “select” route that simply takes the query parameter (or—as in the code below—a dynamic segment), sanitizes it, and sets the property on its parent controller. If the sanitized index is out of bounds it transitions to the nested index route, which in turn transitions back to the nested select route with the default index. The nested routes have no templates or controllers and the parent route has no outlet; they exist solely to handle this routing issue.

// app/router.js
Router.map(function() {
  this.route('shop', function() {
    this.route('index', { path: '/' });
    this.route('select', { path: '/:index' });
  });
});

// app/routes/shop.js
export default Ember.Route.extend({
  setupController(controller, model) {
    this._super(...arguments);

    // some logic to set up the cart service
  },

  model() {
    // fetch products to shop
  }
});

// app/controllers/shop.js
export default Ember.Controller.extend({
  cart: Ember.service.inject(),

  selectedItemIndex: 0,

  actions: {
    selectItem(index) {
      return this.replaceRoute('shop.select', index);
    }
  }
});

// app/routes/shop/index.js
export default Ember.Route.extend({
  beforeModel() {
    return this.replaceWith('shop.select', 0);
  }
});

// app/routes/shop/select.js
export default Ember.Route.extend({
  setupController(_, { index }) {
    this._super(...arguments);

    const
      parentController = this.controllerFor('shop'),
      itemsLength = parentController.get('cart.items.length');

    index = parseInt(index, 10);
    if (Number.isNaN(index) || index < 0 || index >= itemsLength) {
      return this.replaceWith('shop.index');
    }

    parentController.set('selectedItemIndex', index);
  },

  model: _ => _
});

Here’s a workaround:

In the controller, add an observer that changes the query param in the next run loop after it has been set. In this example, I wanted to remove the token query param from the URL and this approach works nicely.

import Ember from 'ember';

export default Ember.Controller.extend({
  queryParams: ['token'],

  token: null,

  unsetToken: Ember.observer('token', function() {
    if (this.get('token')) {
      Ember.run.next(this, function() {
        this.set('token', null);
      });
    }
  }),
});