Mark My Words - built with Laravel and Vue.js

Laravel and Vue.js: creating the Mark My Words web app – Part 3

David Nash laravel, vue.js 0 Comments

If you’d like to start from the beginning, please go back to Part 1 – which covers installing Laravel, connecting to the Oxford Dictionary API, and displaying the results to the user. Then you can move on to Part 2, where I covered creating the database with Laravel’s artisan commands, connecting the back and front ends, and saving the word/definitions for the user.

At the end of Part 2, we’d saved the user’s word/definition to their list. So let’s move on to displaying that list, and allowing the user to delete the word.

Getting all saved words from Laravel

In routes/web.php  we add the following:

Then in app/Http/Controllers/APIController.php, we’ll add the following method:

This is pretty straight-forward, and one of the reasons I love Laravel – you can be very expressive in just a few lines of code. To break it down:

  1. Create a $list  array, so that we can return the items
  2. For each of the logged in user’s words, add it to that list

That’s it! But to make this magic work, we need a couple more lines of code. In app/User.php  add the following relationship:

This says that each user has many UserWords – which is the lookup table that has a row for each user/word combination. The definitions are stored in a separate table, to save space. So in app/UserWord.php we create that relationship with this:

The second and third arguments to hasOne() tell Laravel that the foreign key in the WordDefinition  table is id , and the local key (in the UserWord  object) that matches the WordDefinition  id  is word_id. Yes, that’s a little confusing. Just think of it as saying UserWord.word_id  should match WordDefinition.id.

Above, in APIController.php, our word_list method will now return a JSON array to the logged in user. I don’t think I’ve mentioned it yet, but the way we make sure only logged in users can access methods in APIController.php is with the constructor:

This means that if the user isn’t logged in, they can’t use any of the methods.

Moving Laravel’s data into Vue

Now let’s have a look at the Vue.js code on the front end. In resources/assets/js/app.js, we add code so that when the user logs in, they see all their existing words, which we returned from word_list()  above:

Again we use Axios to get the JSON. We sort them alphabetically. But if we were to do that directly on the app.saved_definitions  variable, Vue wouldn’t know that was happening. So instead we do that on ordered_words, and then updated app.saved_definitions – Vue sort of steps in at this point and realises what’s happening.

I’ve also added a show to each word, so we can keep track of whether or not the user has expanded the definition. I want to be able to just list the words first, and only display a definition when a user taps the word. This keeps things neat, and it aids learning the definition – the user can guess what it is first, before seeing if they were correct.

When the user first logs in, they get their list of words. We don’t need to do anything else to load the saved words from the server. We just make sure that Vue’s saved_definitions will match what’s on the server.

Displaying the words and definitions

In resources/views/home.blade.php, that is covered with this:

Okay, looks kind of complicated. Let’s break it down.

  1. We use Bootstrap’s ul.list-group and li.list-group-item for the formatting.
  2. Each li element is a word in saved_definitions .
  3. The word itself is displayed in the h3  element.
  4. If the user taps or clicks the h3, it toggles the show state for the word, with word.show = !word.show.
  5. We use the span to show the up/down chevron (ie, triangle/arrow).
  6. The @icon  tells Laravel Blade to output the SVG as inline text, rather than using extra requests to the image source. This allows us to use CSS to change the fill colour. I’ve used Blade-SVG to do this.
  7. Under the h3 is the transition, which calls Velocity so that the variable height elements can transition smoothly.
  8. Inside the transition is a div, which show the word definition and a trash icon, so that the user can delete it.

I won’t go into too much depth on the transition, but here’s the code in Vue’s app.methods:

Deleting a word

I like to keep things organised. If a word has been in my list for long enough, I probably know it and don’t need to keep it. The list is a single page and could grow quite long, so let’s allow the user to delete the word. This is done in resources/views/home.blade.php  with this line:

Because we’re using Blade-SVG, we need to add the @click event as a parameter to @icon, but it otherwise works the same – it calls delete_definition in Vue and has the word object as a parameter, which looks like this:

I’m using the native Javascript confirm  method so make sure the user really wants to delete it, as it’s easy to accidentally tap something you didn’t mean. This would be better without the confirmation, but allowing a undo instead. But that would mean some extra work with adding a deleted field to the database, and then actually deleting it once the ‘undo’ button has been dismissed (perhaps after a timeout).

Next we remove the deleted word from saved_definitions with Underscore.js’s filter, and then use Axios to talk to Laravel. To do that, we’ll need another route in routes/web.php:

Then in app/Http/Controllers/APIController.php , we add the following method:

It’s probably possible there’s a more elegant way to do this. First I look at the word for the current user, and then delete it. We don’t really need to return it – in the Vue code above I’m just using console.log()  for debugging purposes.

Conclusion

The finished project is available at mark-my-words.co.

That covers all the major functionality. There are several minor things I haven’t covered. I’m thinking about posting the project to github. If you’d like that, or if you have any questions, please let me know in the comments!

 

Leave a Reply

Your email address will not be published. Required fields are marked *