Making things functional with Vue.js
So, what do we want to achieve with our form? We want the new message to be created. This message has to be composed of title, text, and the timestamp. We also want to add this message to our messages reference array.
Let's call this new message newMessage
and add it to the data
attributes of App.vue
:
//App.vue <script> <...> export default { data () { return { newMessage: { title: '', text: '', timestamp: null } } }, <...> } </script>
Now, let's bind the title and the text of this newMessage
object to input
and textarea
of our form. Let's also bind a method called addMessage
to the submit handler of our form so that the whole form's markup looks like this:
<template> <...> <form @submit="addMessage"> <div class="form-group"> <input class="form-control"v-model="newMessage.title"maxlength="40"autofocus placeholder="Please introduce yourself :)" /> </div> <div class="form-group"> <textarea class="form-control"v-model="newMessage.text" placeholder="Leave your message!" rows="3"></textarea> </div> <button class="btnbtn-primary" type="submit">Send</button> </form> <...> </template>
Well, we have bound the "addMessage"
method to the submit
callback of the form, but we haven't defined this method yet! So, let's define it. Add the methods
object to our App.vue
export section and define the addMessage
method inside it. This method will receive the event attribute from our form and will just grab the newMessage
object and push it into the messagesRef
array. Doesn't it sound easy?
//App.vue <script> export default { <...> methods: { addMessage (e) { e.preventDefault() this.newMessage.timestamp = Date.now() messagesRef.push(this.newMessage) } } } </script>
Now, open the page, fill in the form, and hit the Send button. You'll see your message immediately appearing on the list of messages:
There is still something we need to fix. We don't want the values we fill the form with to remain there after our message is added to the messages list. So, we need to clear it inside the addMessage
method. Probably, some basic check, at least for the title, would also be nice. So, rewrite the method as follows:
//App.vue addMessage (e) { e.preventDefault() if (this.newMessage.title === '') { return } this.newMessage.timestamp = Date.now() messagesRef.push(this.newMessage) this.newMessage.text = '' this.newMessage.title = '' this.newMessage.timestamp = null }
Now, if you start adding more messages, things look a bit weird. The way we're displaying the messages is probably not the best way for our case. Do you remember we wrapped up our message cards into div
with the card-group
class? Let's try to replace it with the card-columns
class and check whether it looks better. In fact, it does. Let's keep it like that.
Adding utility functions to make things look nicer
We already have a fully functional single-page application, but it still lacks some awesomeness. For example, it's not really beautiful that the time appears as a timestamp. Let's write the utility function that will transform our timestamp into something beautiful.
We will use the Moment.js library (https://momentjs.com/). Install it in the application folder:
npm install moment --save
Create a folder and call it utils
. Add a file called utils.js
to this folder. Import moment
and write the following function:
//utils.js
import moment from 'moment'
function dateToString (date) {
if (date) {
return moment(date).format('MMMM Do YYYY, h:mm:ss a')
}
return''
}
Export it in the end of the file:
//utils.js <...> export { dateToString }
Let's import this function to App.vue
and use it to format our timestamp. Open the App.vue
file and add the import
statement at the beginning of the script
section:
//App.vue
<script>
import Firebase from 'firebase'
import { dateToString } from './utils/utils'
<...>
</script>
In order to be able to use this function within the Vue template, we have to export it in the methods
section. Just add a new entry to the methods
object:
//App.vue
<script>
export default {
<...>
methods: {
dateToString: dateToString,
<...>
}
</script>
Since we use ES6, we can just write the following lines of code:
methods: {
dateToString
}
Now, we can use this method inside the template section. Just wrap the message.timestamp
binding object in the dataToString
method:
<p class="card-text"><small class="text-muted">Added on {{ dateToString(message.timestamp) }}</small></p>
Check out the page! Now, you can see beautiful dates instead of Unix timestamps.
Exercise
I have a small exercise for you. You saw how easy it was to add a utility function to transform the timestamp into the nicely formatted date. Now, create another utility function and call it reverse
. This function should be used to display the array of messages in the reversed order, so the most recent messages should appear first. Check the code for this chapter in case you're in doubt.
Extracting message cards to their own component
You probably noticed that the first message of the demo application is always there. It's not moved by other, fresh message items. So, it seems that it's kind of a special message, and it's treated in a special way. In fact, it is. If you want to make a card sticky, just add it before the card
element that iterates through other messages. You can also add some class to this card to show that it's really special. In my case, I added Bootstrap's card-outline-success
class that outlines the element in a nice green color:
//App.vue <div class="card-columns"> <div class="card card-outline-success"> <div class="card-block"> <h5 class="card-title">Hello!</h5> <p class="card-text">This is our fixed card!</p> <p class="card-text"><small class="text-muted">Added on {{ dateToString(Date.now()) }}</small></p> </div> </div> <div class="card" v-for="message in messages"> <div class="card-block"> <h5 class="card-title">{{ message.title }}</h5> <p class="card-text">{{ message.text }}</p> <p class="card-text"><small class="text-muted">Added on {{ dateToString(message.timestamp) }}</small></p> </div> </div> </div>
Now, you have a nice sticky card with a color that differs from other cards' color. But… don't you see any problem? We have the very same code repeated twice in our template. I'm pretty sure that you are aware of the rule of thumb of any developer: DRY—don't repeat yourself!
Let's extract the card to an individual component. It's really easy. Add a component called Card.vue
to the components
folder. The code for this component is really simple:
//Card.vue <template> <div class="card"> <div class="card-block"> <h5 class="card-title">{{ title }}</h5> <p class="card-text">{{ text }}</p> <p class="card-text"><small class="text-muted">{{ footer }}</small></p> </div> </div> </template> <script> export default { props: ['title', 'text', 'footer'] } </script>
Now, let's invoke this component from App.vue
with different values for title, text, and footer. First of all, it should be imported and exported in the Vue components
object:
//App.vue <script> <...> import Card from './components/Card' <...> export default { <...> components: { Card } } </script>
Now, we can use the <card>
element within our template. We need to bind title, text, and footer. Footer is actually the text that says Added on …. So, the markup for the first card will look like this:
//App.vue
<template>
<div class="card-columns">
<card class="card-outline-success":title="'Hello!'":text="'This is our fixed card!'":footer="'Added on ' + dateToString(Date.now())"></card>
</div>
</div>
</template>
The list of other messages will follow the same logic. For each message from the messages
array, we will bind the corresponding message's entries (title, text, and timestamp). So, the markup for the list of message cards will look like this:
<div class="card-columns">
<...>
<card v-for="message in messages":title="message.title":text="message.text":footer="'Added on ' + dateToString(message.timestamp)"></card>
</div>
</div>
As you can see, we have replaced fourteen lines of code with only two lines! Of course, our component also contains some lines of code, but now, we can reuse it again and again.
Exercise
The way we've extracted the card code into its individual component is, without any doubt, great, but the way we are binding attributes for the first message is a bit ugly. What if at some point we need to change the message's text? First of all, it's not easy to find the text inside the markup. Also, it is pretty difficult to manage the text inside the markup attributes, because we have to be really careful not to mess up with double/single quotes. And, admit it, it's just ugly. Your task for this exercise is to extract title, text, and date for the first message into something nicer (for example, export it in the data object) and bind it the same way we bind other messages. If you have doubts regarding this exercise, check out this chapter's code.
Note
Don't be confused by the v-bind
directive in the provided code. We've been using it already, just its shortened version—the name of a bound property written after the semicolon. So, for example, v-bind:messages
is the same as :messages
.