When you think of Web development, formatting data feels like it should be easy. Just concatenate some strings and you're good, right? For years, we mixed markup and code with jQuery and it made it simple...at first. But this ease comes at a cost of code complexity and an inability to test the code. When you first start thinking about how to build applications with a separation of the views and models, it can feel very over-complicated, but once you really start to use it, it will feel more natural and logical than ever going back to writing code in the DOM.

Vue does a good job of separating the two by keeping the markup and the code separate. Even in Single-File Components, this separation is kept clear with two different sections:

<template>
    <div class="row">
        <div class="col-6 offset-3">
            <div>
                <h3>{{ name }}</h3>
            </div>
        </div>
    </div>
</template>

<script type="text/javascript">
export default {
    data: () => {
        return {
            name: "Shawn Wildermuth"
        };
    }
};
</script>

The goal here is to have the model (e.g., the code) expose data that is displayed by the markup (the view). This is important as the view can change the way that data is presented without changing the model. For example, let's say that you have percentageComplete data property that exposes the data as a string. The property then requires that the view only display its text on the view. But if the property were a numeric value, the view could choose whether to show it as text, a graph, a line, or whatever made the most sense. By avoiding doing explicit formatting outside the view, the view gains flexibility of changing the format without making changes in the view model (e.g., the Vue object).

This flexibility between the view and the model is made easier with a way to allow the view to specify how to format the data exposed by the model. This is where Vue Filters come in.

Vue Filters

Filters are a simple mechanism to specify a way to format data in a view. In the markup, you can use it by simply adding a pipe (|) and the name of a filter:

<dd>{{ name | lowerCase }}</dd>

In this example, the filter is named lowerCase. Vue 1.0 shipped with a collection of these filters to use but they decided to drop the built-in filters so you had the flexibility to use whatever filters you wanted. This means that out of the box, you won't have any filters to work with. There are third-party libraries with filters, but in many cases, your filters will be domain-specific (either specific to your business or the application). So you're going to need to build your own filters.

Creating Filters

Filters are a one-way mechanism to take data from a data binding and change the way it's presented to the user. A filter is a tiny piece of code that takes a value and returns a formatted or filtered result. Let's write a filter.

In a component or Vue object, you can create a filters section that contains one or more filters:

...
    filters: {
        lowerCase: function (value) {
            return value.toLowerCase();
        }
    },
...

The filter is executed by name (e.g., lowercase) in the markup itself:

<dd>{{ name | lowerCase }}</dd>

In this example, the name is sent into the filter function and the filter formats it as lowercase. You can have more code than this in a simple filter, but realize that the filter is executed during the rendering of the page, so the more code you write, the more impact it will have on large renderings in Vue.

This method is great for one-off or model-specific filters that you won't need more than once. But in many cases, you'd like your filters to be available throughout your application. That's where global filters come in.

Global Filters

Just like you might build individual components in Vue to share functionality around your application, you'll need to do the same thing with filters. Creating global filters is pretty straightforward. You call Vue.filter and supply a name and function for the filter. For example:

// filters.js
import Vue from "vue";
import Moment from "moment";

Vue.filter("shortDate", function (value) {
    return Moment(value).format("L");
});

In this case, you're using moment.js to format the date into a short date. You're using the shortDate name so you can simply add the filter to the view:

<dd>{{ birthday | shortDate }}</dd>

You can now use the shortDate anywhere in the application. I usually wire it up in the main.js so it can be used by any component or view:

// main.js
import Vue from 'vue';
import App from './App.vue';
import "./filters";

Vue.config.productionTip = false;

new Vue({
    render: h => h(App),
}).$mount('#app');

By importing my filters.js, I have a single place to import them all. But it doesn't really matter how you bring in the filters; by using Vue.filter, you're going to create them globally for your entire application. So far, I've just shown simple filters, but what if you need more control over the filter? That's where parameterized filters come in.

Parameterized Filters

In Vue.js, you can create filters that take one or more arguments. This allows you to have a single filter that can handle multiple use-cases. For example, let's take the shortDate filter and make it allow for more than one type of date filter:

Vue.filter("date", function (value, formatType = "long") {
    if (formatType === "short") {
        return Moment(value).format("L");
    } else if (formatType === "long") {
        return Moment(value).format("LL");
    } else {  
        return "Bad Date Format";
    }
});

In this case, you can take in any number of parameters after the value and use them to determine how to perform the formatting. In this case, you're taking an optional argument for the type of date formatting. Use of the shortDate filter gets changed to use the date filter with a parameter:

<dd>{{ birthday | date("short") }}</dd>

If you were to use only the date filter, it would default to the long format type. This allows you to have any number of parameters that help determine the type of formatting to do.

Filters for Collections

Just like you can have filters for performing actions on implicit types, you can use filters on objects or collections too. For example, let's take a collection of data you might have:

data: () => {
    return {
        name: "Shawn Wildermuth",
        birthday: new Date("1969-04-24"),
        age: 49,
        amountInWallet: 34.99,
        favoriteGames: [
            { name: "Halo", platform: "Xbox"},
            { name: "Fallout 3", platform: "PC"},
            { name: "Legend of Zelda: BOTW", platform: "Switch"},
        ]
    };
},

The favoriteGames property is a collection. You might have a filter that can return a simple count (or other collection operation) on the property:

Vue.filter("count", function (value) {
    return value.length;
});

In this case, the value is still a value you can deal with in a generalized way. You can imagine using underscore or lodash to more complex collection operations inside a filter. The caveat, of course, is that the more complex the filter is, the more pressure it's going to put on rendering complex views.

Computed Values vs. Filters

In most of these cases, you use filters to change the way that data is formatted, but it can be overkill for some cases. If you find yourself using parts of your data object (or the this pointer) as a parameter of a filter, you're doing it wrong. You want to keep domain-specific or view-specific logic out of filters. The dumber they are, the less trouble they'll cause. But you have an alternative when you need the logic in your code: computed values.

The dumber they are, the less trouble your filters will cause.

There's some overlap for computed values and filters, but for the most part, computed values are perfect when what you're doing is specific to the view. For example, you might want to determine if a user is old via a computed value:

...
    computed: { 
        isOld: function() {
            return this.age > 35;
        }
    }
...

In this case, you want to use data that's part of the Vue object (our data) and so a computed value is a lot better than a filter. It works because it has knowledge about the Vue object (or component) so a filter isn't a viable solution. If you need to do this in a generalized way, then a filter that asked you to pass in the age might be useful, but probably not. Try to keep your domain knowledge out of filters. By using a computed value, you can simply use the computed value like any other piece of data:

<dd :class="isOld ? 'isOld' : 'isHip'">  {{ age }}</dd>

Again, you're not going to pollute the code with what the classes are that you need, but instead, just use the computed value to return the answer you need (true or false in this case).

Where Are We?

So, where does that leave us? Hopefully, you've realized that a lot of your job as a Vue.js developer is to keep your code and views separate so that you can continue to maintain, test, and improve your codebase. One key to this strategy is to keep formatting code out of your components as much as possible. I've shown you how to use filters to accomplish a lot of these formatting tasks while keeping these tasks out of the business logic of your applications. In addition, I've shown you that sometimes you need to use business logic in these tasks and that's where computed values can fill the gap.

Listing 1: App.vue

<!-- App.vue -->
<template>
    <div class="row">
        <div class="col-6 offset-3">
            <div id="app">
                <h3>Filters at Work</h3>
                <dl>
                <dt>Name</dt>
                <dd>{{ name | lowerCase }}</dd>
                <dt>Birthday</dt>
                <dd>{{ birthday | date("short") }}</dd>
                <dt>Age</dt>
                <dd :class="isOld ? 'isOld' : 'isHip'">{{ age }}</dd>
                <dt>In Wallet</dt>
                <dd>{{ amountInWallet | currency }}</dd>
                </dl>
                <h4>Games ({{ favoriteGames | count }})</h4>
                    <div v-for="g in favoriteGames" :key="g.name">
                    <div>{{ g.name }} ( {{ g.platform }} )</div>
                    </div>
            </div>
        </div>
    </div>
</template>

<script type="text/javascript">
export default {
    data: () => {
        return {
            name: "Shawn Wildermuth",
            birthday: new Date("1969-04-24"),
            age: 49,
            amountInWallet: 34.99,
            favoriteGames: [
                { name: "Halo", platform: "Xbox"},
                { name: "Fallout 3", platform: "PC"},
                { name: "Legend of Zelda: BOTW", platform: "Switch"},
            ]
        };
    },
    filters: {
        lowerCase: function (value) {
            return value.toLowerCase();
        }
    },
    computed: {
        isOld: function() {
            return this.age > 35;
        }
    }
};
</script>
<style lang="css" scoped>
    .isOld {
        color: red;
    }
    .isHip {
        color: #222;
    }
</style>

Listing 2: filters.js

// filters.js
import Vue from "vue";
import Moment from "moment";

Vue.filter("shortDate", function (value) {
    return Moment(value).format("L");
});

Vue.filter("date", function (value, formatType = "long") {
    if (formatType === "short") {
        return Moment(value).format("L");
    } else if (formatType === "long") {
        return Moment(value).format("LL");
    } else {
        return "Bad Date Format";
    }
});

Vue.filter("count", function (value) {
    return value.length;
});