JAVASCRIPT / GUIDED PROJECT PRACTICE

Build a weather interface with asynchronous data

Build a sample-weather interface locally, with loading, missing-data, and error states.

Why this practice comes now

You have learned forms, asynchronous requests, and local project debugging. Combine them in a sample-weather interface using fixed data, not a live weather service.

Concepts to recall
  • Form input — Read and validate input when the form is submitted.
  • Async control flow — Use await with error handling for asynchronous work.
  • Requests and JSON — Check the response before using its parsed data.
  • Local HTTP — Serve local JSON over HTTP instead of opening it with file://.
  • Testing failures — Test success, missing data, and failure separately.

Start with one action

Download and extract the starter, then inspect its HTML form and script. Begin with blank-input handling and test it before adding any data loading. Use only the supplied sample cities; no account or API key is needed.

Your mission

Build the sample weather interface locally, then replace the memory loader with a local JSON request.

  1. Create the labeled form and copy the script. Test blank input, Manila, Tokyo, surrounding spaces, and an unknown city.
  2. Add the supplied weather.json file and replace loadSample with the fetch version. Run through a local HTTP server.
  3. Rename the JSON file to force failure. Check the error message and re-enabled button, then restore the file and retry successfully.

Build in your own files. The interactive examples below are references to explore. This project has no in-page code editor or automatic solution checks; use the starter download and self-review checklist.

Before you begin: Request and check JSON with fetch.

Unfinished starter files for this project. Edit and save your work on your computer. How to run it on your computer →

Plan your interface states

This final project combines the path's ideas into a weather interface. Begin with two clearly labeled sample cities so you can understand the behavior without a paid service, API key, or changing forecast. The sample is a learning dataset, not current weather. The asynchronous loader imitates a short wait and can later be replaced by a real data source.

Write the states before the implementation. Empty input asks for a city. A valid submission shows loading and disables the submit button. A known city produces a formatted result, while an unknown city gives a useful suggestion. Unexpected failures offer another attempt. Finally restores the control in every outcome, including an early return for missing data.

Keep these tools in mind

Remember first

A useful data interface handles empty input, loading, success, missing data, and failure.

Tools for this project

trim().toLowerCase()Normalize the lookup key
Remove surrounding spaces and use lowercase so Manila and manila find the same sample.
Object.hasOwn(samples, city)Check for an actual city key
Only own keys count as sample cities. Bracket notation then reads the chosen record; otherwise the loader returns null.
condition ? first : secondChoose one expression value
This conditional operator evaluates first when the condition is truthy and second otherwise. Here it returns the matching record or null.
button.disabled = truePrevent duplicate submissions
Keep a loading state while this request is pending. Restore it in finally so failure cannot trap the user.
await loadSample(city)Wait for a loader
The current implementation is an offline promise-based service, not fetch or a live forecast.
typeof data.temperatureValidate before formatting
Require a number and the expected string fields rather than assuming external data is trustworthy.
output.textContentShow plain-text results
City names and descriptions remain text. No user input or service data is parsed as markup.

Try the expected behavior

Build a weather interface with asynchronous data: try it

Predict the result, choose a version, then compare the code and output. Try the controls when the example has them. Each version starts fresh.

Code to notice

const samples = {
  manila: { city: "Manila", temperature: 29, description: "Cloudy" },
  tokyo: { city: "Tokyo", temperature: 21, description: "Sunny" },
};

// Offline teaching service: no network request or API key.
function loadSample(city) {
  return new Promise((resolve) => {
    setTimeout(
      () => resolve(Object.hasOwn(samples, city) ? samples[city] : null),
      300,
    );
  });
}

const form = document.querySelector("#weather-form");
const input = document.querySelector("#city");
const button = form.querySelector("button");
const output = document.querySelector("#weather");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const city = input.value.trim().toLowerCase();
  if (city === "") {
    output.textContent = "Please enter a city.";
    return;
  }
  button.disabled = true;
  output.textContent = "Loading sample weather…";
  try {
    const data = await loadSample(city);
    if (data === null) {
      output.textContent = "No sample for that city. Try Manila or Tokyo.";
      return;
    }
    if (
      typeof data.city !== "string" ||
      typeof data.temperature !== "number" ||
      typeof data.description !== "string"
    ) {
      throw new Error("Unexpected weather data");
    }
    output.textContent = `${data.city}: ${data.temperature} °C, ${data.description} (sample)`;
  } catch {
    output.textContent = "Could not load weather. Please try again.";
  } finally {
    button.disabled = false;
  }
});

Live result

Lesson example
index.htmlPreview

What changed: Try Manila or Tokyo, blank input, and an unknown city. All results are fixed sample data, not current weather.

Start with a labeled form

html
<p><strong>Sample weather data — not a live forecast.</strong></p>
<form id="weather-form">
  <label for="city">City (Manila or Tokyo)</label>
  <input id="city" name="city">
  <button type="submit">Show sample weather</button>
</form>
<p id="weather" role="status">Choose a sample city.</p>

Build it in two stages

To practice actual fetch without credentials, create weather.json with the supplied sample records and serve it from your local HTTP project. Replace only loadSample with the fetch loader below; keep the form and UI states. This separates data access from presentation, making it easier to test both with predictable data before connecting a real service.

For a live forecast, choose a provider, follow its documentation and usage terms, and map its response into your own city, temperature, and description fields. Secret credentials belong on a server, not in downloadable browser code. Decide how to display units and observation time. Keep the sample label until the interface truly uses current data.

A local weather.json dataset

text
{
  "manila": {
    "city": "Manila",
    "temperature": 29,
    "description": "Cloudy"
  },
  "tokyo": {
    "city": "Tokyo",
    "temperature": 21,
    "description": "Sunny"
  }
}

Replace the loader to make a real local request

Replace the entire loadSample function, leaving the form handler in place. The samples constant is no longer needed. This loads fixed sample data over HTTP; it still does not provide live weather. A null value means no matching record, while request or parsing errors are handled by the form's catch.

javascript
async function loadSample(city) {
  const response = await fetch("./weather.json");
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  const records = await response.json();
  if (!records || typeof records !== "object" || Array.isArray(records)) {
    throw new Error("Expected weather records");
  }
  return Object.hasOwn(records, city) ? records[city] : null;
}

Array.isArray rejects an array where we expect a record object. Object.hasOwn checks that the city is an actual stored key rather than an inherited property. These checks make the lookup boundary explicit before the form validates the chosen record's fields.

Run your own files

Extract the starter ZIP and open its folder in your code editor. index.html already loads styles.css and script.js with defer. Implement your handler in script.js. The supplied loadSample function requests weather.json; use a local HTTP server as explained in the fetch lesson. Keep the sample-data label: these records are not a live forecast.

Review local HTTP server setup →

Check the failure paths too

A successful request for an unknown city is different from a network failure. Do not label every failure City not found. Also keep stale requests from overwriting newer ones if you later allow overlapping submissions; this beginner version prevents overlap by disabling submission while loading.

Need a hint?

Open one hint, try a change, and test it before opening the next. The explained example follows separately.

Hint 1

Observe: separate blank input, a known sample city, an unknown city, and a failed request. What message should each produce?

Hint 2

Validate: handle submit and trim the input before calling a loader. How can an early return stop a blank city from starting a request?

Hint 3

Load: keep the loader separate from the display. Check response.ok before parsing JSON; distinguish a missing city from a request that fails.

Hint 4

Recover: trace the disabled button through success and failure. Which part of try/catch/finally runs in both cases? Rename the JSON file, test, restore it, and retry.

Compare with an explained example when you are ready
javascript
const samples = {
  manila: { city: "Manila", temperature: 29, description: "Cloudy" },
  tokyo: { city: "Tokyo", temperature: 21, description: "Sunny" },
};

// Offline teaching service: no network request or API key.
function loadSample(city) {
  return new Promise((resolve) => {
    setTimeout(
      () => resolve(Object.hasOwn(samples, city) ? samples[city] : null),
      300,
    );
  });
}

const form = document.querySelector("#weather-form");
const input = document.querySelector("#city");
const button = form.querySelector("button");
const output = document.querySelector("#weather");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const city = input.value.trim().toLowerCase();
  if (city === "") {
    output.textContent = "Please enter a city.";
    return;
  }
  button.disabled = true;
  output.textContent = "Loading sample weather…";
  try {
    const data = await loadSample(city);
    if (data === null) {
      output.textContent = "No sample for that city. Try Manila or Tokyo.";
      return;
    }
    if (
      typeof data.city !== "string" ||
      typeof data.temperature !== "number" ||
      typeof data.description !== "string"
    ) {
      throw new Error("Unexpected weather data");
    }
    output.textContent = `${data.city}: ${data.temperature} °C, ${data.description} (sample)`;
  } catch {
    output.textContent = "Could not load weather. Please try again.";
  } finally {
    button.disabled = false;
  }
});

The form handler prevents navigation, normalizes the current input, and checks emptiness before starting work. The sample loader returns a promise that settles after a short timer. The handler distinguishes a missing city from invalid data, formats valid fields, and restores the button through finally. Try submitting with Enter as well as clicking.

This worked version uses the offline loader. For the final local-fetch requirement, replace loadSample with the fetch loader shown above and serve the supplied JSON file over HTTP.

Review your project

This is a self-review of the files you built locally. Tuklas cannot inspect those files or automatically check this project. Tick each item only after testing it yourself.

Test each requirement before recording completion.

Choose another practice →