Extend Ember.SortableMixin to allow sorting multiple properties in varying orders


#1

Problem Statement

Sorting inside of Ember.SortableMixin limits us to one order-by clause across multiple properties.

App.AlbumController = Ember.ArrayController.extend({
  sortProperties: ['artistname', 'albumyear', 'id'],
  sortAscending: true
});

This doesn’t account for numerous use cases:

  • Always sort by ID ascending if, for example, album year matches.
  • Ability to strictly enforce stability of sorts.
  • Creating an advanced sort widget (sort by artist name, ascending, then by release year, descending).
  • The entire app that I’m building. :smile:

Proposal

Extend Ember.SortableMixin to allow for sorting direction to be applied on a field-level basis.

API Option 1: Extend the existing sortAscending property to allow passing an array. The push/pop nature of advanced sorting makes this pretty usable. It is also pretty easy to make this backwards compatible and eligible for a point release. Drawback is that it requires two calls to set in order to accomplish one action.

App.AlbumController = Ember.ArrayController.extend({
  sortProperties: ['id'],
  sortAscending: [true]

  actions: {
    sortpush: function(field, ascending) {
      var sortProperties = this.get('sortProperties'),
          sortAscending = this.get('sortAscending');
      
      // Always sort by 'id' last for stability.
      sortProperties.splice(sortProperties.length-1, 0, field);
      sortProperties.splice(sortAscending.length-1, 0, ascending);
    }
  }
});

API Option 2: Create a new sortMultiple key. This is a more declarative approach, in line with what Ember usually does, but it doesn’t integrate as cleanly into the existing API. Still possible to make backwards compatible, but that is only accomplishable where including sortMultiple triggers a sort of run-time deprecation of the older sortAscending approach. I guess it is also possible that sortMultiple could be treated as syntactic sugar for API Option 1, but that would most easily be accomplished using ES6 stuff I don’t think we can use yet.

App.AlbumController = Ember.ArrayController.extend({
  sortMultiple: [
    { property: 'artistname', ascending: false },
    { property: 'id', ascending: true }
  ],

  // sortAscending and sortProperties declarations are ignored, with a bonus debugging assertion.

  actions: {
    sort: function(property, ascending) {
      var sortMultiple = this.get('sortMultiple');
      
      // Always sort by id last for stability.
      sortMultiple.splice(sortMultiple.length-1, 0, { property: field, ascending: ascending });
    }
  }
});

API Option N: This is where you come in. If you don’t like either of my proposed APIs, pitch your own!

I’m also volunteering to write this, so consider this a discussion about whether or not we want to make this change in Ember and what it would look like.

Proof of Need

I am not the only one who has this use case, as demonstrated by this SO post which includes his solution:


#2

Few words on syntax. I like django-framework approach. They use +property or just property for ascending order and -property for descending order. But this might be not so redable of course


#3

@H1D I don’t like combining the signal and control channel into a single field. It can create all sorts of weird edge cases (not saying that it will with this case in particular, but as a pattern I am not a fan).


#4

Sorting by multiple fields in different directions would be another killer feature. I need it myself and I would love to see it in the sortable mixin.


#5

What if you don’t want to sort with a simple ascending/descending, should it be possible to pass in a custom sort function for particular attributes?


#6

@lookingsideways The functionality for custom sorts already exists in the sense that you can define sortFunction… however, that suffers from the same issues as sortAscending: it is only defined once and applied to every field. I hadn’t recognized that limitation, and I’m glad you pointed it out to me. :slight_smile:

In building this it should account for sortFunction per property. This makes me lean even more in the direction of my Option 2 proposal.


#7

After thinking about it a little bit more, I want to be able to directly expose the sort properties to the view to make it easier to allow for user interfaces to use these properties. (Bound, not changed via actions.)

Using that it becomes quite possible to create an advanced sort approach:

Ember.ArrayController.extend({

  /* This assumes that the sorting method for a key remains stable at runtime. */
  sortFunctions: {
    'key1': function(a,b) { return parseInt(a.split('/')[0],10)+parseInt(a.split('/')[1], 10) - parseInt(b.split('/')[0],10)+parseInt(b.split('/')[1], 10); },
    'key2': Ember.compare
  },

  sortFields: this.get('firstObject').keys,

  /* Anything specified here would be the default sort. */
  sortMultiple: [
    { field: 'key1', ascending: true },
    { field: 'key2', ascending: false }
  ],

  actions: {
    add: function() {
      this.get('sortMultiple').push({ field: undefined, ascending: true });
    }
  }
})

{{! And we can bind it to anything we need! }}
{{#each sortMultiple}}
  Sort by: {{view Ember.Select content=controller.sortFields value=field}}
  Ascending: {{input type="checked" value=ascending}}
{{/each}}
<button {{action "add"}}>Add new sort field</button>

This approach allows for undefined to creep into the sortMultiple array as a field, but that can be coded around in the implementation. To make it less twitchy (change and then commit) we can use one-way bindings and an action handler to force the value to update.

In terms of sort patterns, I believe that this is a full and proper enumeration of what is out there. I would like feedback on the proposed API in this post.


#8

List of Sorting Problems

Having thought more about this, here is a collection of the problems that I believe that an updated SortableMixin should solve, or at least account for by providing a recommended pattern:

  • Sort an arbitrary number of fields.
  • Have the ability to set sort precedence (sort by field1, then by field2)
  • Sort the arbitrary number of fields in independent directions (sort by field1 ASC, then by field2 DESC).
  • Ability to use a different comparator for sorting on each field.
  • (Optional) Ability to specify comparator at runtime (sort lexically, sort numerically).

Ember API UI Goals

Not only should we make it possible to accomplish all of the sorting problems we might run into, we should make it easy to use.

  • Handle partial configurations gracefully so that you can ignore most of the API unless you need it.
  • Easy to interface with for creating an advanced sort widget.
  • Provide in the documentation an example for runtime-specified comparator functions.
  • Provide in the documentation an example for live-sorting (fully bound, exposed sorting), and an example for “committed” advanced sort (one which requires an action to save).

(The last two serve as a forcing function to make sure that both patterns are reasonable to write.)


Okay, I think I’m done thinking about the problem, it’s time to code it up. Please reply as to any thoughts you have on the API proposed in the previous post.


#9

If you look at the list of open issues on GitHub regarding SortableMixin, it seems as though the devs are looking to deprecate it ASAP in favour of Ember.computed.sort.

See: https://github.com/emberjs/ember.js/search?q=sortablemixin&ref=cmdform&type=Issues


#10

Ember.computed.sort presently handles all of these.

Ember.computed.sort does not handle this, but I don’t think it is required. Ember.computed.sort's comparator is passed the entire object, not just the property, which I believe obviates the needs for this feature, but am happy to be convinced otherwise.

SortableMixin supports this, but sadly Ember.computed.sort does not. Due to implementation details, this deficiency will be much easier to resolve once https://github.com/emberjs/ember.js/pull/4185 is merged.

As @avaragado mentioned, I would like to deprecate SortableMixin and when doing so, replace its implementation with a simple use of Ember.computed.sort. I believe the only blocker to this is PR #4185.


#11

@hjdivad is there any update on this issue?


#12

So unfortunately the composable computed properties feature was dropped, so I (or someone) will have to update Em.computed.sort manually to support a property comparator.

I guess the tl;dr for your question is “not really”. If anybody wants to take a stab at it I will be happy to help and review; if not it’s just a question of finding time to do so or, similarly, someone for whom the issue is important enough that they’ll pay to have someone work on it.