Ok, so ditto my previous warnings about being new to Vue.js and how my code is probably crap, blah blah blah. You get the idea. I'm learning. I'm sharing. Expect the code not to be perfect. Let's move on!
I began with mobile development using Flex Mobile many, many years ago. I can remember getting a free phone at Adobe MAX (I think that was the year where we also got a Google TV) and downloading the Flex SDK that night in the hotel. The first app I built was something called "INeedIt". The idea was simple - you are in a strange city and need to find... something. A bank. A restaurant. An ATM. The app presented a list of categories, you selected one, and then it would tell you the businesses matching that category that were nearby. You could then click for details.
If you're really curious about the old Flex Mobile version, you can read blog post from way back in 2011. I then rebuilt it in AngularJS in early 2014 and [again](https://www.raymondcamden.com/.../my-perspective-of-working-with-the- ionic-framework/) in Ionic. I thought it would be fun to build it - once again - in Vue.
Before getting into the code, let's look at some screens. This has no design whatsoever, it's just plain text. I've got some ideas about how to make it look nicer, but I thought it made sense to start with the functionality first, and then make it pretty later.
On the first screen, I do two things - load your location via Geolocation, and then present the list of categories.
After selecting a category, the Google Places API is used to find businesses matching that categories within 2000 meters of your location.
Finally you can click on a business for detail. In the screen shot below, you are seeing a small set of the data returned from the API. It's actually quite intensive.
And there you go - that's it. Pretty simple, and no different than what you'd get if you Google for "foo near me", but as an app, it's the perfect kind of thing for me to build to get some practice. Now let's look at the code. I'll be sharing a link both to the online demo and GitHub repo at the end. I'll also remind folks that this is "rough" code. I've already gotten some great feedback from Ted Patrick. His feedback was so darn good, I've decided to turn it into a follow up for later this week.
Let's begin with the HTML, which is rather simple.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<script src="app.js"></script>
</body>
</html>
Basically I just use an empty div and a router-view
. After that I load up Vue, the Vue Router, and my own code. Everything shown on screen will be rendered via Vue components that are loaded based on the particular view of the app. All of my code is in app.js, but let me break it into smaller chunks for easier reading.
First, let's look at the 'meta' code for the application, basically the routing and initial Vue setup.
const router = new VueRouter({
routes:[
{
path:'/',
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
}
]
});
const app = new Vue({
el:'#app',
router
});
I've got 3 routers that map to my three screens. Each uses a particular component. Note the path in the typeList
route. It's rather long. It includes a type id value, a type name, and the location values. I felt a bit weird passing so much in the URL, but then realized that this allowed for bookmarking and sharing, which is pretty cool.
Now let's look at each component. Here is the initial view, ServiceList
.
const ServiceList = Vue.component('ServiceList', {
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>
`,
data:function() {
return {
error:false,
loading:true,
lat:0,
lng:0,
serviceTypes:[
{"id":"accounting","label":"Accounting"},{"id":"airport","label":"Airport"},{"id":"amusement_park","label":"Amusement Park"},{"id":"aquarium","label":"Aquarium"}
]
}
},
created:function() {
console.log('setup for geolocation');
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;
});
}
});
The component consists of a template and then various bits of code to support the functionality. The template has to handle three states:
- initially getting the user's location
- handling an error with that process
- rendering the list of services
You can see the logic to get the user's location in the created
handler. It's just a vanilla Geolocation API call. The service list is a hard coded array of values set in my data function. I've trimmed a significant amount of data from the listing above. It's about 100 items or so. I felt bad embedding that big string in the code. It's ugly. But honestly it felt like the most practical way of handling it. It doesn't change often and making an Ajax call to load it seemed silly. I could move it to the top of my code as a simple constant (I did that for a few other values), but for now I'm just going to live with it.
After you select a service type, the TypeList
component is loaded.
const TypeList = Vue.component('TypeList', {
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>
`,
data:function() {
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']
});
Once again, I start off with a template, a rather simple one, but like before I made it handle a "loading" state so the user knows things are happening. Those "things" are a call to get the results for businesses in this particular category near the location requested. The Google Places API does not support CORS. But guess what? It took me five minutes to write a wrapper in OpenWhisk and IBM Cloud Function. I'm not going to share that code in the post, but it's in the GitHub repo. What's nice is that I not only get around the CORS issue but I can embed my API key there instead of in my code. Alright, now let's look at the final component, Detail
.
const Detail = Vue.component('Detail', {
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>
`,
data:function() {
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']
});
So I've got a few things going on here. In the code, note that I make two manipulations of the data to enable some nice features in the display. First, I rewrite the price_level
value to a string that is more useful (hopefully) to the user. Second, I create an image URL for the location using the Google Static Maps API. I do have my key embedded there but I've locked it down to my domain.
Another interesting thing - note the use of this.$router.go(-1)
for progromatic navigation. Ok I guess it isn't that interesting, it just works, but I dig it!
Want to see this yourself? You can run it online here: https://cfjedimaster.github.io/webdemos/ineedit. The code may be found here: https://github.com/cfjedimaster/webdemos/tree/master/ineedit. Comments and suggestions about my code are definitely welcome. As I said, I already have some great feedback from Ted and I'll be sharing that next!