Dynamic query-params (nested objects) in link-to helper


#1

PREAMBLE: I have read this post which was very helpful, but my problem is slightly more advanced

Using that post I’ve managed to get dynamic query-params working that I can use in the link-to helper. I created a helper mostly as suggested (called dynamic-query-params), and it works well. Nice!

My additional need is to send more complex POJO’s via the link-to query-params. From the /show route I am using link-to to open a new tab target="_blank" that points to the /print route, so that I can generate a print view of the /show but with changes for optimised print. I need the /print route to fetch the same model (except not paginated data), and apply the same config that was on the /show route. Passing this config is the issue.

It comes down to needing to do this;

{{#link-to 'reports.print' report.id (dynamic-query-params filter=filterObject sort=sortObject)}}
Print
{{/link-to}}

filterObject and sortObject change as the user interacts with the /show page. Examples of these two objects are;

filterObject: {
  alarm_state: [1,2,3]
  position: 50
}
sortObject: [{id:'asc'},{position:'desc'}]

When I perform a GET/POST query to the backend, I have extended the JSONAPI adapter to form the URL like so;

/report_data?filter[alarm_state][]=1&filter[alarm_state][]=2&filter[alarm_state][]=3&filter[position]=50&sort=id,-position&page[number]=1&page[size]=5

I want the same to occur for link-to’s query-params - what’s the best way to serialize this, and then deserialize those query-params at the /print route? Is there some kind of adapter I can modify to do the same for query-params? Should I override the link-to helper?

I can probably hack together a solution, I am just not sure of the ember-y way to do this.


#2

serializeQueryParam and deserializeQueryParam methods exist for the Route, but they are private…


#3

For anyone visiting this looking for a solution, I found a decent way to do it that doesn’t make me cringe

So I ended up using a combination of a helper (to flatten the object keys in preparation for query params) and then ember-parachute to serialize the values

:warning: Wall of code incoming :warning:

templates/reports/show.hbs

{{!-- target _blank so new window/tab, no access to unsaved data, need to send config via qp's --}}
{{#link-to 'reports.print' reportModel.id
    (hash
      isQueryParams=true
      values=(flatten-object-keys pageConfig)
    )
target="_blank"}}
  Print
{{/link-to}}

helpers/flatten-object-keys.js

/*
TURN THIS
// pageConfig
{
	filter: {
		alarm_state: [1,2,3],
		something_else: {
			a: 1
		}
	},
	sort: {
		local: [{},{}],
		backend: [{},{}]
	}
}
INTO
{
	'filter[alarm_state]': [1,2,3],
	'filter[something_else][a]': 1,
	'sort[local]': [{},{}],
	'sort[backend]': [{},{}]
}
*/
export default Helper.extend({
  compute(params/*, hash*/) {
    let [object] = params;
    return this.serializeObjectParam(object);
  },

  serializeObjectParam(object,parentKey) {
    let result = {};
    Object.keys(object).forEach((key) => {
      let value = object[key];
      // If the value is an object, keep flattening
      if(typeOf(value) === 'object') {
        let flattened = this.serializeObjectParam(value,key);
        result = assign(result,flattened);
      } else {
        // Reached a primitive or array, stop here and add the flattened key
        result[parentKey?parentKey+'['+key+']':key] = value;
      }
    });
    return result;
  }
});

That’s the flattening of the object taken care of. link-to should now accept it as query params. Now to serialize the query param values with ember-parachute. This is taken care of automatically by ember-parachute, as long as you have a serialize method supplied for each key, in the controller of the route you are linking to! i.e. /print controller

Additionally, ember-parachute also handles deserialization on the other side.

controllers/reports/print.js

export const validQueryParams = new QueryParams({
  filterAlarmState: {
    as: 'filter[alarm_state]',
    defaultValue: [],
    replace: true,
    serialize(array) { // Used by link-to helper to create any URL to this route!
      return (array && typeOf(array) === 'array')?array.join(','):array;
    },
    deserialize(arrayString = '') {
      return arrayString.split(',').map((item) => parseInt(item, 10)).sort();
    }
  },
  sortLocal: {
    as: 'sort[local]',
    defaultValue: [],
    replace: true,
    serialize(sortKeysArray) { /* serialize here */ },
    deserialize(arrayString) { /* deserialize here */ },
  },
  sortBackend: {
    as: 'sort[backend]',
    defaultValue: [],
    replace: true,
    serialize(sortKeysArray) { /* serialize here */ },
    deserialize(arrayString) { /* deserialize here */ },
  }	
});

export default Controller.extend(validQueryParams.Mixin, {
  reportModel: undefined,	// Set by the route
});

routes/reports/print.js

export default Route.extend({
  model(params) {
    this.set('reportId',params.report_id);
    return [];
  },

  setupController: function(controller, model) {
    this._super(controller, model);	
    let reportId = this.get('reportId');

    // By now, the controller should have access to the deserialized ember-parachute query params.
    // let alarmState = controller.get('filterAlarmState');

    this.store.findRecord('report',reportId).then((report) => {
      controller.set('reportModel',report);
    });
  }
});

And here’s a sample of the URL that link-to displays (I’ve decoded the URL - don’t worry it will be encoded) /reports/:report_id/print?filter[alarm_state]=1,2,3&sort[backend]=-name&sort[local]=id,-tyre_position