Mark My Words - built with Laravel and Vue.js

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

David Nash laravel, vue.js 5 Comments

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 &ldquo;<span class="search_term">@{{ last_search_term }}</span>&rdquo;:</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 &ldquo;<span class="search_term">@{{ last_search_term }}</span>&rdquo;.</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

  1. Pingback: Laravel and Vue.js: creating the Mark My Words web app - Part 2

  2. Pingback: Laravel and Vue.js: creating the Mark My Words web app - Part 3

  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

    1. Post
      Author

      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.

Leave a Reply

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