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.