Dec 16, 2015 | vuejs, javascript

Writing a case-insensitive orderBy filter with VueJS

!
Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.

There's a cool new open source project called koel that came out this week. It's a music player (a Spotify clone) that serves your own music and runs on Laravel and VueJS. I figured I'd try it out and see if I could send in any pull requests to help out.

One thing I noticed was that when I sorted by artist (or anything else), any items that start with a lowercase letter were sorted after all the items that started with an uppercase letter. I took a look to see what was happening, and it seems that VueJS' orderBy function is case sensitive by default, which mean uppercase letters get sorted first, and then lowercase. I Googled around and found a closed GitHub issue that indicated that this was the intended behavior for orderBy, so I set out to write a case-insensitive orderBy filter.

Finding Vue's native orderBy

First, I knew that I wanted to mimic Vue's native orderBy function as closely as possible. So, I needed to hunt it down. There are various ways to do that—grep, your IDE, whatever else—but eventually we land on vuejs/vue/src/filters/array-filters.js.

/**
 * Filter filter for arrays
 *
 * @param {String} sortKey
 * @param {String} reverse
 */

export function orderBy (arr, sortKey, reverse) {
  arr = convertArray(arr)
  if (!sortKey) {
    return arr
  }
  var order = (reverse && reverse < 0) ? -1 : 1
  // sort on a copy to avoid mutating original array
  return arr.slice().sort(function (a, b) {
    if (sortKey !== '$key') {
      if (isObject(a) && '$value' in a) a = a.$value
      if (isObject(b) && '$value' in b) b = b.$value
    }
    a = isObject(a) ? getPath(a, sortKey) : a
    b = isObject(b) ? getPath(b, sortKey) : b
    return a === b ? 0 : a > b ? order : -order
  })
}

What's this doing? At its core, it's duplicating the array and sorting it by pulling out the values of the provided key and comparing it.

You may have noticed that we have a few non-native functions in use: convertArray, isObject, and getPath.

How to add a custom filter

So, we know the structure of our new filter. Where do we put it?

Vue makes it simple to add a custom filter. Here's the structure, which you can place in whatever file you're using to do your core Vue bindings:

Vue.filter('reverse', function (value) {
    return value.split('').reverse().join('');
});

Let's try an example where we do something to an array. Notice we want to duplicate the array with .slice() so we're not manipulating the original array.

Vue.filter('uppercaseArray', function (array) {
    return array.map(function (item) {
        return item.toUppercase();
    });
});

We mapped over each item in the duplicated array, assumed it was a string, uppercased it, and then returned the mapped array. That means we can now, anywhere in our app, use this filter:

<tr v-for="item in items | uppercaseArray"> ... etc.

Note: In this example, we didn't use slice() before operating on the array. Why? Because, as wonderful folks pointed out to me on Twitter, map() in JavaScript already creates a duplicate, so you don't need slice() like you do with sort().

Porting and updating orderBy

Great. Now, let's write our own. Let's take orderBy and add a few lines to make it case insensitive.

Vue.filter('caseInsensitiveOrderBy', function (arr, sortKey, reverse) {
  arr = convertArray(arr)
  if (!sortKey) {
    return arr
  }
  var order = (reverse && reverse < 0) ? -1 : 1
  // sort on a copy to avoid mutating original array
  return arr.slice().sort(function (a, b) {
    if (sortKey !== '$key') {
      if (isObject(a) && '$value' in a) a = a.$value
      if (isObject(b) && '$value' in b) b = b.$value
    }
    a = isObject(a) ? getPath(a, sortKey) : a
    b = isObject(b) ? getPath(b, sortKey) : b

    // Our new lines
    a = a.toLowerCase()
    b = b.toLowerCase()

    return a === b ? 0 : a > b ? order : -order
  })
});

As you can see, I just added the lower-casing at the end, nothing else. What happens?

ERROR. Uncaught ReferenceError: convertArray is not defined. Well, crap.

Turns out all those custom functions aren't just sitting around for you—you have to find where they're available. I was able to find all of them except convertArray (I found it but I think it may be an entirely private method) so let's update it with the place Vue exposes each (getPath I found with Evan's help on GitHub, and isObject I found by trial and error).

Vue.filter('caseInsensitiveOrderBy', function (arr, sortKey, reverse) {
  // arr = convertArray(arr)
  if (!sortKey) {
    return arr
  }
  var order = (reverse && reverse < 0) ? -1 : 1
  // sort on a copy to avoid mutating original array
  return arr.slice().sort(function (a, b) {
    if (sortKey !== '$key') {
      if (Vue.util.isObject(a) && '$value' in a) a = a.$value
      if (Vue.util.isObject(b) && '$value' in b) b = b.$value
    }
    a = Vue.util.isObject(a) ? Vue.parsers.path.getPath(a, sortKey) : a
    b = Vue.util.isObject(b) ? Vue.parsers.path.getPath(b, sortKey) : b

    a = a.toLowerCase()
    b = b.toLowerCase()

    return a === b ? 0 : a > b ? order : -order
  })
});

As you can see, some of those core Vue methods are exposed via objects like Vue.util and Vue.parsers.

Now, let's just take a look at convertArray to see if we care. Turns out it's an alias for _postProcess:

_postProcess: function _postProcess(value) {
  if (isArray(value)) {
    return value;
  }
  ... etc
}

Well, check it out! This isn't perfect, but if we're passing in an array, we just get the array back. So, while I'll still keep looking at how to bring in convertArray properly, we can drop it safely for any use of this new function that is, indeed, getting an array passed in.

And that's it. We now have a functional caseInsensitiveOrderBy filter.

<tr v-for="item in items | caseInsensitiveOrderBy title"> ... etc.

Good. To. Go.


Comments? I'm @stauffermatt on Twitter


Tags: vuejs  •  javascript

Subscribe

For quick links to fresh content, and for more thoughts that don't make it to the blog.