Yet Another Update to my INeedIt Vue.js App

This is the last update to my INeedIt app - I promise. At least until I get another idea or two for a good update. But then that will be the last one - honest. (Ok, probably not. ;) Before I begin, be sure to read the first post about this demo and the update from a few days ago. The last update was relatively minor. This one is pretty radical.

One of the things I ran into when working on this app was that my app.js file was getting a bit large. To be clear, 260 lines isn't really large per se, but it gave me a slight code smell to have my components and main application setup all in one file. I especially didn't like the layout portions. While template literals make them a lot easier to write, having my HTML in JavaScript is something I'd like to avoid. Heck, just the lack of color coding is a bit annoying:

No color coding here

The solution is single file components. As you can guess, they let you use one file per component. Here is a trivial sample.


<template>
<div>
  <strong>My favorite pie is {{pie}}.</strong>
</div>
</template>

<script>
module.exports = {
  data:function() {
    return { pie:'pecan' };
  }
}
</script>

<style scoped>
div {
  background-color: yellow;
}
</style>

Each single file component (SFC) may be comprised of a template (layout), script (logic) and style (design) block.

So this is cool - however - using this form requires a build process of some sort. I'm definitely not opposed to a build process, but I was a bit hesitant to move to one for Vue. I loved how simple Vue was to start with and I was concerned that moving to a more complex process wouldn't be as much fun. Plus, I found the docs for SFC to be a bit hard to follow. In general, I love the Vue docs, but I was a bit loss in this area as it assumes some basic familiarity with Webpack.

When I first encountered this, I stopped what I was doing and tried to pick up a bit of Webpack. I ran across an incredibly good introduction on Smashing Magazine: Webpack - A Detailed Introduction. This gave me enough basic understanding to be a bit more familiar with how to use it with Vue. I still don't quite get everything, but I was able to build a new version of the app using SFCs and the Webpack template.

To begin working with the Webpack template, you need the Vue CLI. The CLI is a scaffolding and build tool. You can install it via npm: npm i -g vue-cli. Then you can scaffold a new app via vue init webpack appname. This will run you through a series of questions (do you want to lint, do you want to test, etc), and at the end, you've got a new project making use of SFCs.

The new project is a bit overwhelming at first. Maybe I just suck, but it was rather involved. You've got a built in web seerver, auto reload, and more, but in the end it felt much like working with Ionic. I'd edit a file and Webpack would handle all the work for me.

So how did I build INeedIt in this template? The template has a main App component that simply creates a layout and includes a router-view. As I learned earlier this month, that's how the Vue router knows where to inject the right component based on the current URL. I removed that image since I didn't have anything that was always visible.

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

I then began the process of creating a SFC for each of my three views. For the most part, this was literally just copying and pasting code into new files. Here is ServiceList.vue:

<template>
    <div>
    <h1>Service List</h1>
        <div v-if="loading">
        Looking up your location...
        </div>

        <div v-if="error">
        I'm sorry, but I had a problem getitng your location. Check the console for details.
        </div>

        <div v-if="!loading && !error">
        <ul>
        <li v-for="service in serviceTypes" :key="service.id">
            <router-link :to="{name:'typeList', params:{type:service.id, name:service.label, lat:lat, lng:lng} }">{{service.label}}</router-link>
        </li>
        </ul>
        </div>

    </div>
</template>

<script>
export default {
    name:'ServiceList',
    data () {
        return {
            error:false,
            loading:true,
            lat:null,
            lng:null,
            serviceTypes:[
                {"id":"accounting","label":"Accounting"},{"id":"airport","label":"Airport"},{"id":"amusement_park","label":"Amusement Park"},{"id":"aquarium","label":"Aquarium"},
            ]
        }
    },
    mounted:function () {
        this.$nextTick(function () {
            if (this.lat === null) {
                console.log('get geolocation', this.lat);
                let that = this;
                navigator.geolocation.getCurrentPosition(function (res) {
                    console.log(res);
                    that.lng = res.coords.longitude;
                    that.lat = res.coords.latitude;
                    that.loading = false;
                }, function (err) {
                    console.error(err);
                    that.loading = false;
                    that.error = true;
                });
            }
        })
    }
}
</script>

Note - as before I've removed 90% of the serviceTypes values to save space. Next I built TypeList.vue:

<template>
    <div>

        <h1>{{name}}</h1>

        <div v-if="loading">
        Looking up data...
        </div>

        <div v-if="!loading">
            <ul>
                <li v-for="result in results" :key="result.id">
                <router-link :to="{name:'detail', params:{placeid:result.place_id} }">{{result.name}}</router-link>
                </li>
            </ul>

            <p v-if="results.length === 0">
            Sorry, no results.
            </p>

            <p>
            <router-link to="/">Back</router-link>
            </p>
        </div>

    </div>
</template>

<script>
const SEARCH_API = 'https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/googleplaces/search.json';

// used for search max distance
const RADIUS = 2000;

export default {
    name:'ServiceList',
    data () {
        return {
            results:[],
            loading:true
        }
    },
    created:function () {
        fetch(SEARCH_API +
        '?lat=' + this.lat + '&lng=' + this.lng + '&type=' + this.type + '&radius=' + RADIUS)
        .then(res => res.json())
        .then(res => {
            console.log('res', res);
            this.results = res.result;
            this.loading = false;
        });
    },
    props:['name','type','lat','lng']
}
</script>

And finally, here is Detail.vue:

<template>
    <div>
        <div v-if="loading">
        Looking up data...
        </div>

        <div v-if="!loading">

            <div>
                <img :src="detail.icon">
                <h2>{{detail.name}}</h2>
                <p>{{detail.formatted_address}}</p>
            </div>

            <div>

                <p>
                    This business is currently 
                    <span v-if="detail.opening_hours">
                        <span v-if="detail.opening_hours.open_now">open.</span><span v-else>closed.</span>
                    </span>
                    <br/>
                    Phone: {{detail.formatted_phone_number}}<br/>
                    Website: <a :href="detail.website" target="_new">{{detail.website}}</a><br/>
                    <span v-if="detail.price">Items here are generally priced "{{detail.price}}".</span>
                </p>

                <p>
                <img :src="detail.mapUrl" width="310" height="310" class="full-image" />
                </p>

            </div>

            <p>
            <a href="" @click.prevent="goBack">Go Back</a>
            </p>

        </div>
    </div>
</template>

<script>
const DETAIL_API = 'https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/googleplaces/detail.json';

export default {
    name:'Detail',
    data () {
        return {
            detail:[],
            loading:true
        }
    },
    methods:{
        goBack:function () {
            this.$router.go(-1);
        }
    },
    created:function () {
        fetch(DETAIL_API +
        '?placeid=' + this.placeid)
        .then(res => res.json())
        .then(res => {
            console.log('res', res.result);
            /*
            modify res.result to include a nice label for price
            */
            res.result.price = '';
            if (res.price_level) {
                if (res.result.price_level === 0) res.result.price = "Free";
                if (res.result.price_level === 1) res.result.price = "Inexpensive";
                if (res.result.price_level === 2) res.result.price = "Moderate";
                if (res.result.price_level === 3) res.result.price = "Expensive";
                if (res.result.price_level === 4) res.result.price = "Very expensive";
            }
            this.detail = res.result;

            // add a google maps url
            this.detail.mapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${this.detail.geometry.location.lat},${this.detail.geometry.location.lng}&zoom=14&markers=color:blue%7C${this.detail.geometry.location.lat},${this.detail.geometry.location.lng}&size=310x310&sensor=true&key=AIzaSyBw5Mjzbn8oCwKEnwI2gtClM17VMCaNBUY`;
            this.loading = false;
        });
    },
    props:['placeid']
}
</script>

Finally, I modified router/index.js, which as you can guess handles routing logic for the app.

import Vue from 'vue'
import Router from 'vue-router'
import ServiceList from '@/components/ServiceList'
import TypeList from '@/components/TypeList'
import Detail from '@/components/Detail'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'ServiceList',
      component: ServiceList
    },
    {
            path:'/type/:type/name/:name/lat/:lat/lng/:lng',
            component:TypeList,
            name:'typeList',
            props:true
    },
    {
            path:'/detail/:placeid',
            component:Detail,
            name:'detail',
            props:true
        }

  ]
})

All I did here was import my components and set up the path.

And that was it! Ok, I lie. When I made my app I accepted the defaults for ESLint and it was quite anal retentive about what it wanted, which is to be expected, but I disabled a lot of the rules just so I could get my initial code working. In a real app I would have kept the rules.

I got to say... I freaking like it. I still feel like it's a big step up from "just include vue in a script tag", but as I worked on the app it was a great experience. If you want to see the final code, you can find it here: https://github.com/cfjedimaster/webdemos/tree/master/ineedit3

Take a look at the dist folder specifically. This is normally .gitignore'd, but I modified the settings so it would be included in the repo. You'll see that Webpack does an awesome job converting my code into a slim, optimized set of files. You can actually run the demo here: https://cfjedimaster.github.io/webdemos/ineedit3/dist/index.html

Finally, take a look at Sarah Drasner's article on the Vue CLI: Intro to Vue.js: Vue-cli and Lifecycle Hooks Her entire series on Vue is definitely worth reading.

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also