v0.7 - Watchers, Root Events and Form Validation
In this section we are going to create our own Concerts, and in the process, learn about how form interaction works in Vue.
Lets start by moving the ConcertView component into its own file. Lets call that file ConcertView.js
Just a simple copy and paste.
ConcertView.js
const ConcertView = Vue.component('concert-view', {...})
If we refresh we will see the same list we had before.
Now lets create our new component ConcertCreate.js. Duplicate the ConcertView file we just made and open it. Replacing ConcertView with ConcertCreate and removing all the ConcertView specific stuff.
ConcertCreate.js
const ConcertCreate = Vue.component('concert-create', {
props: {}
},
components: {
BaseButton
},
computed: {},
methods: {},
template: `
<div class="card">
<base-button :content="'Submit'" @clicked=""/>
</div>
`
})
We are going to be creating a component that creates a new concert so lets leave the button in there and change the content to Submit.
Now lets import these into our index.html file, right before app.js. We need to make sure they are in before because, app.js depends on this component (for the list of concerts) and it must be defined first.
index.html
<!-- Vue.js -->
<script type="text/javascript" src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<script type="text/javascript" src="BaseButton.js"></script>
<script type="text/javascript" src="ConcertView.js"></script>
<script type="text/javascript" src="ConcertCreate.js"></script>
<script type="text/javascript" src="app.js"></script>
And we will plop it in right above our list of events.
index.html
<div class="button"
@click="toggleSort()">
{{sort_btn_text}} ↓
</div>
<concert-create></concert-create>
<concert-view v-for="concert in filtered_concerts"
:concert="concert"
:key="concert.id">
</concert-view>
We will also need to register the component on the application's components object
const app = new Vue({
el: '#app',
components: {
ConcertView,
ConcertCreate
},
Refreshing the page we will now see a beautiful box with a button in it.
Lets start building the form, we are going to need a
- Name
- Location
- Date & Time
In our template that looks like this..
ConcertCreate.js
template: `
<div class="card">
Who's playing?
<input
type="text"
placeholder="Band name"
/>
Where's it at?
<select>
<option>
Place
</option>
</select>
When are you gonna be there?
<div>
<input type="date" style="width:40%">
at
<input type="time" style="width:40%">
</div>
<base-button :content="'Submit'" @clicked=""/>
</div>
`
We are going to need somewhere to store the data from those fields though
So just like in our button component we are going to need a data function that sets up our initial state. Lets also wire those up to their inputs using v-model
Since we are doing date & time, we are going to have to get a bit fancy, what we are doing here is admittedly a bit hack and I would recommend something different for a production application but this is ok for now. We are taking a new date object and using the .toISOString function to get a string like this "2018-07-13T01:12:25.592Z". We are then splitting at the 'T' and taking the first element to get just "2018-07-13".
Since most shows start sometime after 7:30 lets just hardcode that one as '19:30' and leave it as is. The HTML input element knows how to handle military time and we will see it come up as 7:30 PM in the element.
ConcertCreate.js
data() {
return {
name: '',
place: '',
start_date: new Date().toISOString().split('T')[0],
start_time: '19:30',
}
},
template: `
<div class="card">
Who's playing?
<input
type="text"
v-model="name"
placeholder="Band name"
/>
Where's it at?
<select v-model="place">
<option>
Place
</option>
</select>
When are you gonna be there?
<div>
<input type="date" style="width:40%" v-model="start_date">
at
<input type="time" style="width:40%" v-model="start_time">
</div>
<base-button :content="'Submit'" @clicked=""/>
</div>
`
Since name is just a simple text input field we dont need to do much with it beyond what we have here, but the locations are best if they are structured as a Dropdown. This way we can match them with events we already have!
To make this happen we need a list of venues that the user can select from, lets make a prop called venues that we will pass a list of Venue names into.
ConcertCreate.js
const ConcertCreate = Vue.component('concert-create', {
components: {...},
props: {
venues: {
type: Array,
required: true
}
},
data() {...},
template: `...`
})
And lets zoom out to our app.js file and create a computed property that cleans up our events into a simple list of Venue name strings.
const app = new Vue({
el: '#app',
components: {...},
computed: {
venues() {
const set = {};
for (var i = 0; i < this.concerts.length; i++) {
set[this.concerts[i].place.name] = true;
}
return Object.keys(set).sort();
},
}
})
Here we are using the keys of an object to get unique values in a list. We start by looping over the list of concerts using a standard for loop, then getting a little fancy and using a Prototype on Object called Keys to grab they keys of the Object. This will return to us an array of the keys of our set object. We can then use a simple default sort to put these into alphabetical order.
There are a bunch of ways to do this type of filtering, and this is somewhere in the middle in the readability/speed spectrum. The really nice part about this approach is it is pretty versatile, fast and can be translated 1:1 into most other languages.
Lets pass that computed property into the <concert-create> component using the venues property we set up
index.html
<concert-create :venues="venues"></concert-create>
Now that we have our beautiful list of venues we can link it up to our form so that the dropdown shows the values.
ConcertCreate.js
template: `
...
Where's it at?
<select v-model="place">
<option v-for="v in venues" :value="v">
{{v}}
</option>
</select>
...
`
})
Watchers
If we reload we will see when we click the dropdown that the list has a bunch of venues in it, which is pretty neat, but initially it is set to nothing, which isnt great- if we can set a default we should.
We can use a watch function to set the default to the first venue when the list loads in from the gist.
Lets create a key called watch and have it watch venues, and when venues changes set this.place to the value of the first element in the list.
Watch functions are handy for updating data when you are not exactly sure when something will happen. Whether this is data from another component as in our case or some kind of user interaction we are waiting on.
ConcertCreate.js
...
watch: {
venues() {
this.place = this.venues[0];
},
},
...
Watch functions may seem really similar to computed functions. You are right! They are incredibly similar, in fact most times you go to use a watch function you should as yourself if it can be done as a computed function. Watch's come with a few advantages, and are generally used in cases where you are creating side effects from a change, as we are here by setting this.place.
When we reload we will see the default venue value is showing on the display.
Lets send this data over to our list of concerts so that we can see it!
Root Events
ConcertCreate.js
...
methods: {
submit() {
const payload = {
name: this.name,
place: {
name: this.place,
},
start_time: `${this.start_date}T${this.start_time}`
}
this.$root.$emit('new_concert', payload);
// reset our name to an empty string
this.name = '';
}
},
template: `
<div class="card">
...
<base-button :content="'Submit'" @clicked="submit"/>
</div>
`
...
In this function we are constructing a payload from the data we have collected from the form, mocking the format that we know from the concerts we have in the list.
Then we are using the instances $root property to emit an event. $root gives us access to our root Vue instance and gives us a simple way to pass this new concert back to the list.
So we are calling the event new_concert and giving our payload as the second argument. once that is sent off we are setting name back to an empty string.
Now lets set up a listener in app.js to receive this event.
app.js
...
created() {
this.getConcerts();
this.$root.$on('new_concert', (c) => {
this.concerts.unshift(c);
})
},
...
We register the listener in our created lifecycle hook so that it gets set only once. Array.unshift is basically the opposite of Array.push. Instead of tacking items onto the end of an array it shoves them in at the beginning.
This would matter if our list wasn't sorted, but since we are using a computed variable to sort and filter our concert list it wont matter.
Unfortunately our current set up would allow the user to create an event without a band name! To make sure they dont, we can set up a computed variable that will indicate when the form is valid.
Since all of our values are just strings our check is pretty simple, we can make a condition making sure the length of all our values is at least 1. The !! is a simple way of forcing the values between the brackets to cast to a boolean.
ConcertCreate.js
...
computed: {
valid() {
return !!(
this.name.length &&
this.place.length &&
this.start_date.length &&
this.start_time.length
)
}
},
...
methods: {
submit() {
if (!this.valid) return;
const payload = {
name: this.name,
place: {
name: this.place,
},
start_time: `${this.start_date}T${this.start_time}`
}
this.$root.$emit('new_concert', payload);
// reset our name to an empty string
this.name = '';
}
},
...
We are also modifying the submit function here to return if the form's data is not valid. This is great but what about the user? How do they know the button is disabled cause of the data.
We could hide our button when this value is false but I feel that is kind of ugly, it will pop in and cause a mess. Lets add a state to our button that indicates that the button is not clickable. We will call it enabled
ConcertCreate.js
...
template: `
<div class="card">
...
<base-button :content="'Submit'"
:enabled="valid"
@clicked="submit"
/>
</div>
`
...
Within our BaseButton component we need to set up a prop called enabled and apply a style change when it is false.
We can do something interesting here and set an optional property, supplying a default value of true to be used if enabled isn't given.
BaseButton.js
const BaseButton = Vue.component('base-button', {
props: {
content: {...},
enabled: {
type: Boolean,
default: true,
required: false
}
},
data() {...},
computed: {
loading_background() {
return {
'disabled-background': !this.enabled,
'loading-background': this.loading
}
}
},
methods: {
click() {
if (!this.enabled) return;
this.loading = true;
setTimeout(() => {
this.$emit('clicked');
this.loading = false;
}, 1000)
}
},
template: `...`
})
Similarly to the loading background class we will apply a class if the button is not enabled.
We have a class called disabled-background that changes the color of the button to a lighter green and keeps the cursor from changing to a 👆.
Just as we did in the ConcertCreate component lets restrict the click method to only run when enabled is true.
If we reload we will now see that unless we have the name value set, the button remains the lighter green color and does not do anything if we click.