Weather Search App with Vue 3 + OpenWeatherMap API
Hey y’all! Today’s objective is to build a basic weather search app using Vue 3 + OpenWeatherMap API. Over the course of this project, we’ll look at the following:
- How to set up a search() function to handle a response from the OpenWeatherMap API based on a user-inputted US zip code.
- How to employ the JavaScript Object.keys() method in a function to round weather values (returned from the response) to the nearest whole number.
The app that we’ll build is actually a stripped-down, easy-to-setup, single-page version of a multi-faceted OpenWeatherMap API dashboard app I built a while ago. Y’all can check out that repo in GitHub if you’d like. For this project, however, we’ll keep things simple.
And yes…apparently it DOES get that cold in Arlington, TX. ❄️❄️❄️
L️et’s get started!
Project Setup
First, open up your IDE and copy and paste the following template code to an HTML file:
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="demo" class="min-h-full bg-gray-200">
<div class="bg-gray-300 flex justify-center">
<div class="w-full max-w-md">
<div class="space-y-6 m-8">
<div class="rounded-md shadow-sm">
<div>
<input @keypress="search" v-model="cityZip" type="text" pattern="\d*" maxlength="5"
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" placeholder="Enter Zip Code Here..." />
</div>
</div>
</div>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<div class="text-left">
<div class="p-6">
<div class="p-12 bg-white shadow overflow-hidden sm:rounded-lg">
<div>
<h2 v-else class="text-4xl font-extrabold tracking-tight text-gray-900">{{ cities || '?' }}</h2>
<p class="mt-2">{{ dateTime || '?' }}</p>
<dl class="mt-8 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 sm:gap-y-10 lg:gap-x-8">
<div class="border-t border-gray-200 pt-4">
<dt class="font-large text-md text-gray-500">Temp</dt>
<dd class="mt-2 font-medium text-7xl text-gray-900">{{ weatherData.temp || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Max/Min</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ weatherData.temp_max || '?' }}°F/ {{ weatherData.temp_min || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Feels Like</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ weatherData.feels_like || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Humidity</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ weatherData.humidity || '?' }}%</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
Next, we’ll set up the script block:
<script>
const { onMounted, ref, computed } = Vue
const app = Vue.createApp({
setup() {
// Add Functionality Here
}
})
app.mount('#demo')
</script>
Inside the setup() function, add the following properties:
setup() {
const cities = ref()
const weatherData = ref({})
const dateTime = ref(null)
const cityZip = ref('')
//Add search() function Here
return { cities, weatherData, dateTime, cityZip, search }
}
Search() Function
Below the properties, we’ll set up the search() function that will handle the OpenWeatherMap API call:
const search = async (e) => {
if (e.key == "Enter") {
try {
const res = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?zip=${cityZip.value},US&appid={addyouropenweatherapikeyhere}&units=imperial`
)
cities.value = res.data.name
weatherData.value = res.data.main
const date = Date(res.data.dt).toLocaleString("en-US")
dateTime.value = date.toString()
cityZip.value = ''
} catch (error) {
console.log(error, 'error found!')
}
}
}
For the API call to work, you’ll need to register for an API key on the OpenWeatherMap website. After setting up an account and retrieving the key, copy and paste it into the API URL where specified.
Upon entering a 5 digit zip code and hitting Enter, the API call will return weather data for a user-inputted zip code (see the interpolated cityZip.value property in the URL). To see the parsed data, simply copy and paste the URL with your API key and a hard-coded zip code into the browser. You’ll then see a large JSON tree of weather data for your selected city.
For this app, we are mainly concerned with getting the weather data contained in the following object from the API response:
"main": {
"temp": 10.67,
"feels_like": -1.93,
"temp_min": 9.28,
"temp_max": 12.25,
"pressure": 1038,
"humidity": 55
},
While these values are in Celsius by default, we can convert them to Fahrenheit by adding “&units=imperial” at the end of the API URL. After saving res.data.main to weatherValue.data, we should be able to see those values converted to Fahrenheit.
Now run the app in your browser. At this point, you should see a view similar to this:
This seems to work fine. However, one of the objectives of this project is to return the weather values rounded to the nearest whole number. We can accomplish this by employing an Object.keys() method as part of the function to handle the rounding of the weather values. But first, let’s review the basic concept of the Object.keys() method.
Object.keys() Method
If you aren’t familiar, The Object.keys() method in JavaScript returns an array of a given object's own enumerable string-keyed property names. Observe the following example from the JavaScript MDN docs:
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
object1 is an object that contains properties. Each property is represented by a key:value pair. The outcome of this log is a new array containing only the keys from the original object. In this case, the output would be:
> Array ["a", "b", "c"]
Now let’s apply that concept to our app. Back in our HTML file, add the following below the search() function:
const roundedValue = () => {
const roundData = {}
Object.keys(weatherData.value).map(key => {
console.log(key)
return roundData[key] = Math.round(weatherData.value[key]);
})
return roundData
}
Inside the roundedValue() function, we’ll create a property called roundData, which we’ll set to an empty object. We’ll then use the Object.keys() method to create a new array containing all of the keys from weatherData.value.
console.log(Object.keys(weatherData.value)) should return the following:
['temp', 'feels_like', 'temp_min', 'temp_max', 'pressure', 'humidity']
Next, we’ll use a map() method to iterate over the newly-created key array, with each item in the key array being represented by the key element. To round each weather value, we’ll pass weatherData.value[key] into Math.round() inside the callback, while using the key element as an index for each iterated item in weatherData.value. Finally, we’ll assign each newly rounded value to roundData[key]. After running console.log(roundData), we should now see a new object (based on values from another search, FYI):
{
temp: 22,
feels_like: 10,
temp_min: 19,
temp_max: 25,
pressure: 1040,
humidity: 37
}
In the setup() function, we’ll add a new computed property called roundedData, which will return our roundedValue() function. Let’s not forget to include both of those items in the setup return.
setup() {
const roundedData = computed(() => roundedValue())
const cities = ref()
const weatherData = ref({})
const dateTime = ref(null)
const cityZip = ref('')
//...search() function
//...roundedValue() function
return { cities, weatherData, dateTime, cityZip, search, roundedData, roundedValue }
}
Let’s also update the weather info card in the template, so as to interpolate the roundedData computed property (where appropriate):
<div class="mx-auto max-w-4xl px-6">
<div class="text-left">
<div class="p-6">
<div class="p-12 bg-white shadow overflow-hidden sm:rounded-lg">
<div>
<h2 v-else class="text-4xl font-extrabold tracking-tight text-gray-900">{{ cities || '?' }}</h2>
<p class="mt-2">{{ dateTime || '?' }}</p>
<dl class="mt-8 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 sm:gap-y-10 lg:gap-x-8">
<div class="border-t border-gray-200 pt-4">
<dt class="font-large text-md text-gray-500">Temp</dt>
<dd class="mt-2 font-medium text-7xl text-gray-900">{{ roundedData.temp || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Max/Min</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ roundedData.temp_max || '?' }}°F/ {{ roundedData.temp_min || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Feels Like</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ roundedData.feels_like || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Humidity</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ roundedData.humidity || '?' }}%</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
The final code should look like the following:
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="demo" class="min-h-full bg-gray-200">
<div class="bg-gray-300 flex justify-center">
<div class="w-full max-w-md">
<div class="space-y-6 m-8">
<div class="rounded-md shadow-sm">
<div>
<input @keypress="search" v-model="cityZip" type="text" pattern="\d*" maxlength="5"
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" placeholder="Enter Zip Code Here..." />
</div>
</div>
</div>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<div class="text-left">
<div class="p-6">
<div class="p-12 bg-white shadow overflow-hidden sm:rounded-lg">
<div>
<h2 v-else class="text-4xl font-extrabold tracking-tight text-gray-900">{{ cities || '?' }}</h2>
<p class="mt-2">{{ dateTime || '?' }}</p>
<dl class="mt-8 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 sm:gap-y-10 lg:gap-x-8">
<div class="border-t border-gray-200 pt-4">
<dt class="font-large text-md text-gray-500">Temp</dt>
<dd class="mt-2 font-medium text-7xl text-gray-900">{{ roundedData.temp || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Max/Min</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ roundedData.temp_max || '?' }}°F/ {{ roundedData.temp_min || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Feels Like</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ roundedData.feels_like || '?' }}°F</dd>
</div>
<div class="border-t border-gray-200 pt-4">
<dt class="font-medium text-md text-gray-500">Humidity</dt>
<dd class="mt-2 text-4xl text-gray-900">{{ roundedData.humidity || '?' }}%</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { onMounted, ref, computed } = Vue
const app = Vue.createApp({
setup() {
const roundedData = computed(() => roundedValue())
const cities = ref()
const weatherData = ref({})
const dateTime = ref(null)
const cityZip = ref('')
const search = async (e) => {
if (e.key == "Enter") {
try {
const res = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?zip=${cityZip.value},US&appid={addyouropenweatherapikeyhere}&units=imperial`
)
cities.value = res.data.name
weatherData.value = res.data.main
const date = Date(res.data.dt).toLocaleString("en-US")
dateTime.value = date.toString()
cityZip.value = ''
} catch (error) {
console.log(error, 'error found!')
}
}
}
const roundedValue = () => {
const roundData = {}
console.log(Object.keys(weatherData.value))
Object.keys(weatherData.value).map(key => {
console.log(Math.round(weatherData.value[key]))
return roundData[key] = Math.round(weatherData.value[key]);
})
return roundData
}
return {
cities,
weatherData,
dateTime,
cityZip,
search,
roundedData,
roundedValue
}
}
})
app.mount('#demo')
</script>
The final output should be similar to the first screenshot:
And there you have it! Congrats! 🔥🔥🔥
Thanks for following along! Be sure to follow my blog and clap for my posts! Also, I’d love to see your feedback on what y’all think about the blog, and what improvements can be made (if any).
Stay tuned for more killer posts. Follow me on Twitter.😎
Other posts from this author: