After having lots of fun using Laravel to build Three Good Things, I naturally started looking for other fun projects I could build.
I don’t really watch TV — I prefer to read. At the start of the year I was thinking about new year’s resolutions and decided that whenever I read a word I didn’t know, I would write it down. I quickly discovered that looking up a word and it’s definition was a fairly major interruption to whatever I was reading, especially using mobile to copy+paste into a Google Keep note.
It seemed like a fairly simple thing for an app to do, so I tried to find one. I was surprised to find that most of the vocabulary apps were quite complex. Many wouldn’t allow you to enter words – they would provide their own each day. Some had the feature I wanted, but they were an add-on. I decided this would be perfect for a little Laravel app, and I could use it to learn Vue.js at the same time.
The finished app is available at https://mark-my-words.co – please check it out and let me know what you think!
Getting started with Laravel
I started by installing a fresh copy of Laravel and created the user system with php artisan make:auth.
The first step was to create a text field for the word the user is looking up. I added search_term to Vue’s data object, and set v-model=”search_term” on the input.
I created a search function in Vue’s methods object. This makes a GET request to my API, which in turn queries the Oxford Dictionary’s API for a list of definitions.
So in my routes/web.php I added Route::get(‘api/search’, ‘APIController@search’); and used php artisan make:controller APIController to create a controller that would contain all the API methods. I looked into Laravel’s API Authentication, but it was overkill – I wanted to use the web authentication to handle this – the user is already logged in, so there’s no point in authenticating twice. To take care of this I added:
public function __construct() { $this->middleware('auth'); }
to app/Http/Controllers/APIController.php.
Getting a word definition
In APIController.php I created a search(Request $request) method. I used Guzzle to send the request to the Oxford Dictionary API.
The code looks like this:
public function search(Request $request) { $word_id = strtolower(urlencode(trim($request->search_term))); //guzzle client $url = 'https://od-api.oxforddictionaries.com/api/v1/entries/en/'.$word_id; $client = new Client(['http_errors'=>false]); $response = $client->get($url,[ 'headers' => [ 'app_id' => 'redacted_app_id', 'app_key' => 'redacted_app_key', ] ]); if( $response->getStatusCode() != 200 ) return []; $list = []; $data = json_decode( $response->getBody() ); }
The $word_id is sanitised: trim any spaces, urlencode it, and convert it to lowercase.
Next I had to process the JSON that was returned. Oxford Dictionary returns a LOT of information, so I had to use this unfortunate code to achieve that:
//oh gross if( isset( $data->results ) ) { foreach( $data->results as $r ) { if( isset( $r->lexicalEntries ) ) { foreach( $r->lexicalEntries as $l ) { if( isset( $l->entries ) ) { foreach( $l->entries as $e ) { if( isset( $e->senses ) ) { foreach( $e->senses as $s ) { if( isset( $s->definitions ) ) { foreach( $s->definitions as $d ) { $list[] = [ 'word' => $r->word, 'definition' => $d, ]; } } } } } } } } } }
Yep, I know. This just didn’t seem right to me. I asked about it on Stack Overflow and unfortunately there’s not much that can be done to improve it.
After this I trim any trailing periods from the definition and simply return $list;.
Back in resources/assets/js/app.js, here’s the search function that gets triggered when the form is submitted:
search: function() { if( this.search_term.length === 0 ) return; this.last_search_term = this.search_term; this.loading = true; this.$refs.search_field.focus(); axios.get('/api/search', { params: { search_term: this.search_term } }).then( function(response) { if( response.data.length === 0 ) { app.no_results = true; app.show_results = false; } else { app.results_list = []; response.data.forEach( function(result) { app.results_list.push({ word: result.word, definition: result.definition }); }); app.no_results = false; app.show_results = true; } app.loading = false; }); },
There are a few data variables that I haven’t mentioned yet: this.last_search_term is separate from this.search_term so that it’s doesn’t “re-actively” update when the user edits the input field. this.loading simply shows a loading icon while the search is performed. no_results and show_results tell the app if it should show a “no results found” message, or show the results. app.results_list shows those results.
Displaying the results
Next let’s have a look at the code to display the results in resources/views/home.blade.php:
<div v-cloak> <transition @enter="slide_down" @leave="slide_up" :css="false"> <div class="results" v-if="show_results"> <h3>Results for “<span class="search_term">@{{ last_search_term }}</span>”:</h3> <div class="list-group"> <div v-for="item in results_list" @click="save_definition(item)" class="list-group-item clearfix"> <div class="pull-left definition">@{{ item.definition }}</div> <div class="pull-right word_action">@icon('add-solid', 'action')</div> </div> </div> </div> <div class="no-results" v-if="no_results"> <h3>Nothing found for “<span class="search_term">@{{ last_search_term }}</span>”.</h3> </div> </transition> </div><!-- v-cloak -->
To run through the above code, I use v-cloak to hide the block until it’s rendered, because otherwise it shows up when the page loads, and doesn’t look pretty. In my CSS, I use this:
[v-cloak] { display: none !important; }
Next is the transition – I wanted the results to slide down when they’re loaded. I normally would have reached for jQuery to do this, but I’m trying to avoid jQuery as it’s an additional library for just a few effects. So instead I used Velocity.js which I import with window.Velocity = require(‘velocity-animate’); at the top of app.js.
Then in my Vue methods object I have:
slide_down: function(el, done) { Velocity(el, 'slideDown', { duration: 'fast', complete: done }); }, slide_up: function(el, done) { Velocity(el, 'slideUp', { duration: 'fast', complete: done }); }
These get triggered by the @enter and @leave events.
Finally, a user clicks on a definition to save it to their list, which I’ll discuss in Laravel and Vue.js: creating the Mark My Words web app – Part 2.
Have you used Laravel and Vue.js to create a web app? Do you think I could have done any of this better? Please let me know in the comments!
Comments 5
Pingback: Laravel and Vue.js: creating the Mark My Words web app - Part 2
Pingback: Laravel and Vue.js: creating the Mark My Words web app - Part 3
Hey there,
Just reading your article, pretty good. But I also noticed a bug on your site, the code block would actually cover the navigation bar due to the z-index property on crayon-syntax class when scrolling. I know it’s not related to the article, but I think it would be good to point that out for you haha.
Cheers
Thanks for pointing that out, He! Overly large z-index values are a pet peeve of mine. They should only be as high as needed and no higher! I’ve updated the CSS so the navbar should be on top now. Thanks again.
Hey, what Version of laravel & vue J’s that you use to build this project?