Intro
In the first three parts of this series we looked at an approach for building an autocomplete drop-down by starting with a basic component and adding functionality to it. We used v-model
support in Vue.js to provide a consistent interface between our component and its parent.
In this post we will use the same principles to implement a date range picker. Our component will include a mix of custom code and third party libraries but we will ensure it is well-encapsulated and reusable.
Getting started
Back in Part 2 I outlined 4 principles for setting up a component to use v-model
:
- Determine how your component’s current value will be represented (e.g. string, number, custom object)
emit
an “input” event when the selected value changes (how and when you do this may change depending on the details of your component)- Provide a
value
prop which accepts valid values for your component - Ensure that the controls in your component reflect this value when mounted and when the value of the prop changes.
These principles can help to keep your components concerns cleanly separated and makes it easier to reuse a given component in other parts of your site.
This becomes even more crucial when your components aren’t just small wrappers around HTML elements but instead are larger compound components with a mix of custom functionality and third party libraries.
We will be building a Date Range picker. This control allows a user to quickly specify a start and end date either by selecting from a pre-set list of common ranges (Yesterday, Last Week, Last Month, This Month, etc.) or by entering a custom start and end date.
This is how it looks in action:
Let’s get started by creating an empty DateRangePicker.vue
file in our project:
and adding it to our Filter.vue
template:
Note: if you’ve been following along, you already have a bunch of code in Filters.vue
. I’ve omitted that here for clarity but feel free to leave it in. It will work fine with our additions.
We will hook this up so it displays a value as we go along.
1. Determine how your component’s current value will be represented
For our use here let’s define a date range as a pair of Date
s. We’ll encapsulate these in an object with the format:
We will emit an object in this format when our component’s value changes and a parent can set our component’s value with an object in this format.
2. emit
an “input” event when the selected value changes
Pre-set range types
The simplest way for a user to select a value with our component is to choose a range type from a pre-set list. Our component will calculate an appropriate value to match the range type and emit
that.
The set of range types we will support is:
- Yesterday
- Today
- This Week
- Last Week
- This Month
- Last Month
- Last 3 Months
- Last 6 Months
Let’s add those as a data property:
then render a select
with those options:
We’ll also add a handler for when the value of this select changes:
Calculating ranges with moment.js
The beauty of Vue.js is that it makes it easy to compose well-encapsulated components that use a mix of built-in, custom, and third party functionality.
For our range calculations we will use moment.js.
Begin by installing the moment.js package with npm:
Then we can import it into our component:
With moment.js it’s ridiculously easy to generate correct from and to values for our various range types:
Displaying the selected range
At this point we can add a v-model
directive to our component and see our picker in action:
Much better.
Picking custom ranges
At this point we have a useful, but basic, component. We are going to enhance its functionality by letting the user specify a custom date range using a nice-looking date picker. As we do this, note how we don’t have to make any changes to our implementation in Filters.vue
.
This is the key of this whole series. We want to create components that are well-encapsulated so that we can enhance them over time without having to rewrite the parent components.
We will use the vuejs-datepicker
library for our start and end dates and v-model their values to a pair of data properties:
These look good but we only want to show them when the user specifically selects “Custom” as the range type. We need to add an option for “Custom” to our dropdown and v-model
the selectedRangeType
to a data property so we can use it to control the display of the “From” and “To” options in our template:
Emitting custom ranges
Note that the user can pick From and To dates for their custom range we need to properly handling the emitted value. Right now, we are emitting an input
event when the user selects “Custom Range…”. Since calculateRangeForType
doesn’t have a case for custom
our component emit
s null
.
Our desired
behaviors are:
- If the user picks a pre-set range type, emit a matching range value
- If the user picks “Custom Range..” and either From or To is still empty, do nothing
- If the user picks “Custom Range..”, and From or To are both provided, emit a matching range value
- If the user changes a picked date value, and From or To are both provided, emit a matching range value
If the user picks a pre-set range type, emit a matching range value
The first behavior is already working. Rock.
If the user picks “Custom Range…” and either From or To is still empty, do nothing
Let’s add a check to dateRangeChanged()
to handle this:
If the user picks “Custom Range..”, and From or To are both provided, emit a matching range value
This can be handled by adding an else
block to the conditional we just added:
If the user changes a picked date value, and From or To are both provided, emit a matching range value
At this point we will only see our custom range if the user selects a pre-set range and then switches it back to “Custom Range…”. We can fix this and handle the last behavior by binding to the date picker’s input events:
Whew! We now have pickable custom date ranges!
3. Provide a value
prop which accepts valid values for your component
The other half of implementing v-model
is adding a value
prop which the parent can use to set the value of your component.
For many simple cases you can simple add the prop by name like so:
However, recall that we are emitting values in this format:
We should add a validation function for our prop to ensure that our code is providing correctly formatted values. Let’s structure our prop definition like so:
For our component, null
is valid and means “No date range selected”. Otherwise, we expect to have an Object
with two properties, from
and to
and the values of those properties should be Date
s:
You can quickly test this validation function by editing the selectedDatePrange
property in Filters.vue
.
Try setting it to:
and check your console. You should see an error:
Now, change selectedDateRange
to:
You should see no errors.
4. Ensure that the controls in your component reflect this value when mounted and when the value of the prop changes.
We can now ensure that our component responds appropriately when the parent changes the value
prop. Typically this means updating the state of the component’s UI to reflect the new value.
We will do this in a new updateComponentWithValue
method:
We have two cases to handle. The first case is if the new value is null
. In that case we want to ensure that our select and both of the date pickers are set to null:
The second case is if a specific range is provided. Since the value will only have a from
and to
date provided, it isn’t simple to determine what pre-set range type is represented. For now, we will treat any provided range as a custom range:
We then need to make sure we call this method when the component is mounted:
and any time the value
prop changes:
Summary
We now have full-featured date range picker which includes multiple ways to specify the range and includes a nice looking third-party calender control.
Note how all of the logic for selecting a date is encapsulated within our component. The parent component, Filters.vue
is not involved at all with the specifics of how the user chooses each date. It only cares about what the chosen range is.
Also, note how the Date Range Picker does not specify how the resulting dates range is formatted. Filters.vue
contains all of the logic for formatting the value in its selectedDateRangeFormatted
computed value.
This clean separation of concerns using v-model as the interface layer is very powerful. It helps to avoid creating components that are either too specific to a particular use case that they can’t be reused or too intertwined with their parent component that the break when included somewhere else.
What we have is generic and reusable.
Additionally, we can continue to improve the behavior of our component over time without necessarily needing to constantly revisit the parent component’s behavior.
Bonus: handling pre-set range types
In step 4 above I implemented updateComponentWithValue
in a naive way which doesn’t take into account pre-set range types at all. This was fine for demonstration purposes but isn’t very satisfying. It’s especially noticeable if we add a second Date Range Picker to Filters.vue
:
So, let me show you how we can add in support for parsing a pre-set range type from just a from
and to
date. This will help to drive home how we can constantly improve our components without messing with their parents.
Our approach will be to look at the incoming date range and compare its values to what the current value of each pre-set range is, in turn. This is easy to do with our calculateRangeForType
method:
Let’s break that down.
First, we loop over all of our pre-set range types:
For each one, get the pre-set date range that goes with it:
One of the range type options is ‘custom’ which will doesn’t have a pre-set date range, it will just be null
. If we see that, just skip it:
Next, we take the from
and to
components of the new date range value and compare them to the values form the pre-set range:
If they match, then we set the selected range type in our drop-down to the current range type in our loop and clear out the date picker values.
If we loop through all of the pre-set range types and don’t find a match, then this is a custom date and we can set it like so:
Now our component behaves quite nicely with lots of them on the page.
When you are building your own reusable components it is critical to test them out with multiple instances bound to the same value. You can catch a lot of inconsistencies in your implementation this way.
Add new comment