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:
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.