Table of Contents
- Prerequisites
- Setting Up the Vue.js Project
- Planning the App Structure
- Building Core Components
- Integrating a Weather API
- Styling the App
- Adding Advanced Features
- Testing the App
- Deploying the App
- Troubleshooting Common Issues
- Conclusion
- References
Prerequisites
Before diving in, ensure you have the following:
- Node.js & npm/yarn: Install from nodejs.org (LTS version recommended).
- Basic Vue.js Knowledge: Familiarity with Vue components, reactivity, and directives (e.g.,
v-model,v-if). - Code Editor: VS Code (with the Volar extension for Vue 3 support) or your preferred editor.
- API Key: Sign up for a free API key at OpenWeatherMap (we’ll use their Current Weather Data API).
Setting Up the Vue.js Project
We’ll use Vue CLI to scaffold the project quickly.
Step 1: Install Vue CLI
If you don’t have Vue CLI installed, run:
npm install -g @vue/cli
# or
yarn global add @vue/cli
Step 2: Create a New Project
Run vue create weather-app and follow the prompts:
- Select Vue 3 (recommended for this tutorial).
- Choose Default ([Vue 3] babel, eslint) for simplicity.
Step 3: Navigate to the Project and Run the Dev Server
cd weather-app
npm run serve
Your app will run at http://localhost:8080. Open this in your browser to see the default Vue welcome page.
Planning the App Structure
A well-organized app is easier to maintain. Here’s our plan:
Key Components
- SearchBar: Input field to search for cities.
- WeatherDisplay: Shows weather data (temperature, description, icon, etc.).
- ErrorAlert: Displays errors (e.g., invalid city).
- App.vue: Parent component that manages state and API calls.
State Management
For simplicity, we’ll use Vue’s built-in reactivity (no Vuex/Pinia needed) since the app is small. We’ll store:
city: Current search query.weatherData: Fetched weather data (temperature, humidity, etc.).error: Error message (if any).
Building Core Components
Let’s start building the components.
1. The SearchBar Component
This component lets users input a city and trigger a search.
Create src/components/SearchBar.vue:
<template>
<div class="search-bar">
<input
type="text"
v-model="city"
placeholder="Enter city name..."
@keyup.enter="handleSearch"
/>
<button @click="handleSearch">Search</button>
</div>
</template>
<script>
export default {
name: "SearchBar",
data() {
return {
city: "", // Binds to input field
};
},
methods: {
handleSearch() {
if (this.city.trim()) {
// Emit event to parent (App.vue) with the city
this.$emit("search", this.city.trim());
this.city = ""; // Clear input after search
}
},
},
};
</script>
<style scoped>
.search-bar {
display: flex;
gap: 8px;
margin: 20px 0;
}
input {
padding: 8px 12px;
flex: 1;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
button {
padding: 8px 16px;
background: #42b983; /* Vue's brand color */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #359469;
}
</style>
2. The WeatherDisplay Component
This component receives weatherData as a prop and displays it.
Create src/components/WeatherDisplay.vue:
<template>
<div class="weather-display" v-if="weatherData">
<h2>{{ weatherData.name }}, {{ weatherData.sys.country }}</h2>
<div class="temp">
{{ Math.round(weatherData.main.temp) }}°C
</div>
<div class="description">
<img
:src="`https://openweathermap.org/img/wn/${weatherData.weather[0].icon}@2x.png`"
alt="Weather icon"
/>
<p>{{ weatherData.weather[0].description }}</p>
</div>
</div>
</template>
<script>
export default {
name: "WeatherDisplay",
props: {
weatherData: {
type: Object,
required: true,
},
},
};
</script>
<style scoped>
.weather-display {
text-align: center;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.temp {
font-size: 48px;
font-weight: bold;
margin: 10px 0;
}
.description {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 18px;
text-transform: capitalize;
}
</style>
3. The ErrorAlert Component
Shows errors (e.g., “City not found”).
Create src/components/ErrorAlert.vue:
<template>
<div class="error-alert" v-if="message">
⚠️ {{ message }}
</div>
</template>
<script>
export default {
name: "ErrorAlert",
props: {
message: {
type: String,
required: true,
},
},
};
</script>
<style scoped>
.error-alert {
color: #dc3545;
background: #f8d7da;
padding: 12px;
border-radius: 4px;
margin: 10px 0;
text-align: center;
}
</style>
4. Updating App.vue
Now, import and use the components in src/App.vue:
<template>
<div class="app">
<h1>Vue Weather App</h1>
<SearchBar @search="fetchWeather" />
<ErrorAlert :message="error" v-if="error" />
<WeatherDisplay :weatherData="weatherData" v-if="weatherData" />
</div>
</template>
<script>
import SearchBar from "./components/SearchBar.vue";
import WeatherDisplay from "./components/WeatherDisplay.vue";
import ErrorAlert from "./components/ErrorAlert.vue";
export default {
name: "App",
components: {
SearchBar,
WeatherDisplay,
ErrorAlert,
},
data() {
return {
weatherData: null, // Holds fetched weather data
error: "", // Holds error message
};
},
methods: {
async fetchWeather(city) {
// We'll add API logic here next!
},
},
};
</script>
<style>
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
</style>
Integrating a Weather API
We’ll use OpenWeatherMap’s API to fetch weather data.
Step 1: Get Your API Key
- Go to OpenWeatherMap API and sign up.
- After logging in, go to API Keys and generate a new key (it may take 10–15 minutes to activate).
Step 2: Fetch Weather Data in App.vue
Update the fetchWeather method in App.vue to call the API:
async fetchWeather(city) {
const apiKey = "YOUR_API_KEY"; // Replace with your key!
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;
try {
this.error = ""; // Clear previous errors
const response = await fetch(url);
if (!response.ok) throw new Error("City not found");
const data = await response.json();
this.weatherData = data; // Update weatherData
// Save to localStorage for persistence
localStorage.setItem("lastCity", city);
} catch (err) {
this.error = err.message;
this.weatherData = null; // Clear stale data
}
}
Step 3: Load Last Search on App Start
Add mounted() to App.vue to load the last searched city from localStorage:
mounted() {
const lastCity = localStorage.getItem("lastCity");
if (lastCity) {
this.fetchWeather(lastCity); // Auto-load last search
}
}
Styling the App
Let’s polish the UI with better styling. Update App.vue’s style:
.app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
min-height: 100vh;
}
h1 {
color: #2c3e50;
margin-bottom: 30px;
font-size: 2.5rem;
}
/* Center content vertically */
@media (min-width: 768px) {
.app {
padding: 40px;
}
}
Update WeatherDisplay.vue’s style for a card-like look:
.weather-display {
text-align: center;
padding: 30px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-top: 20px;
}
.temp {
font-size: 4rem;
font-weight: 700;
color: #2c3e50;
margin: 20px 0;
}
.description p {
font-size: 1.2rem;
color: #7f8c8d;
text-transform: capitalize;
}
Adding Advanced Features
1. Show Additional Data (Humidity, Wind)
Update WeatherDisplay.vue to include humidity and wind speed:
<div class="details">
<div class="detail-item">
<span class="label">Humidity:</span>
<span>{{ weatherData.main.humidity }}%</span>
</div>
<div class="detail-item">
<span class="label">Wind:</span>
<span>{{ weatherData.wind.speed }} m/s</span>
</div>
</div>
Add styles for .details in WeatherDisplay.vue:
.details {
display: flex;
justify-content: space-around;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.detail-item {
color: #555;
}
.label {
font-weight: bold;
margin-right: 4px;
}
2. Unit Conversion (Celsius ↔ Fahrenheit)
Add a toggle to switch between Celsius and Fahrenheit.
In App.vue, add a isCelsius data property:
data() {
return {
// ... existing data
isCelsius: true, // Track unit preference
};
}
Add a button in App.vue’s template to toggle units:
<button class="unit-toggle" @click="isCelsius = !isCelsius">
{{ isCelsius ? "°C" : "°F" }}
</button>
Update WeatherDisplay.vue to use the unit:
<div class="temp">
{{ Math.round(isCelsius ? weatherData.main.temp : (weatherData.main.temp * 9/5) + 32) }}
{{ isCelsius ? "°C" : "°F" }}
</div>
Pass isCelsius as a prop to WeatherDisplay:
<WeatherDisplay :weatherData="weatherData" :isCelsius="isCelsius" v-if="weatherData" />
Update WeatherDisplay’s props to accept isCelsius:
props: {
// ... existing props
isCelsius: {
type: Boolean,
required: true,
},
},
Testing the App
Test the app by:
- Searching for valid cities (e.g., “London”, “Tokyo”).
- Searching for invalid cities (e.g., “Atlantis”) to trigger errors.
- Toggling units and verifying temperatures update.
- Refreshing the page to ensure the last city loads from
localStorage.
Deploying the App
Deploy your app for free using Netlify:
- Push your code to a GitHub repo.
- Go to Netlify → “Add new site” → “Import an existing project” → Connect your GitHub repo.
- Set build settings:
- Build command:
npm run build - Publish directory:
dist
- Build command:
- Click “Deploy site”. Netlify will generate a URL (e.g.,
your-app-name.netlify.app).
Troubleshooting Common Issues
- API Key Not Working: Ensure your key is activated (wait 15 minutes) and correctly pasted.
- CORS Errors: OpenWeatherMap’s API supports CORS, so this shouldn’t happen. If it does, use a proxy (e.g.,
https://cors-anywhere.herokuapp.com/before the URL). - Stale Data: Clear
weatherDataon errors to avoid showing old data.
Conclusion
You’ve built a fully functional weather app with Vue.js! You learned:
- How to structure Vue components and manage state.
- Integrate third-party APIs with error handling.
- Add persistence with
localStorage. - Style and deploy a Vue app.
Next steps: Add a 5-day forecast, location-based weather (using Geolocation API), or dark mode!