Getting fancier – a basic weather application

It should be quite obvious that Svelte has built up its compiler to work with most of the modern ECMAScript standards. One area where they do not provide any sort of wrapper is for fetching data. A good way to add this and see the effects is to build a basic weather application.

A weather application, at its core, needs to be able to take in a zip code or city and spit out information about the current weather for that region. We can also get an outlook for the weather based on this location. Finally, we can also save these choices in the browser, so we can use them when we come back to the application.

For our weather data, we are going to pull from https://openweathermap.org/api. Here, the free service will allow us to get the current weather. On top of this, we will need an input system that will accept the following:

  • The city/country
  • The zip code (if no country is given, we will assume US since that is the default for the API)

When we enter the correct value, we will store it in LocalStorage. Later in the chapter, we will take a more in-depth look at the LocalStorage API, but just note that it is a key-value storage mechanism in the browser. When we go to enter a value for the input, we will get a drop-down of all of our previous searches. We will also add the ability to remove any one of these results from the list.

First, we need to get an API key. To do so, follow these steps:

  1. Go to https://openweathermap.org/api and follow the instructions to get an API key.
  2. Once we have created an account and verify it, we will be able to add API keys.
  3. After login, there should be a tab that says API keys. If we go to that, we should be greeted with a no api keys message.
  4. We can create a key and add a name to it if we want (we can just call it default).
  5. With this key, we are now able to start calling their server.

Let's go ahead and set up a test call. The following code should work:

let api_key = "<your_api_key>";
fetch(`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${api_key}`)
.then((res) => res.json())
.then((final) => console.log(final));

If we put this into a code snippet, we should get back a JSON object with a bunch of data inside of it. Now we can move onto utilizing Svelte with this API to create a nice weather application.

Let's set up our application in the same way we set up our Todo application. Run the following commands:

> cd ..
> npx degit sveltejs/template weather
> cd weather
> npm install
> npm run dev

Now that we have started the environment, let's create a boilerplate application with some basic styling. In the global.css file, add the following lines to the body:

display: flex;
flex-direction : column;
align-items : center;

This will make sure our elements are both column-based and that they will start from the center and grow out. This will give us a nice look for our application. Next, we are going to create two Svelte components, a WeatherInput and a WeatherOutput component. Next, we are going to focus on the input.

We will need to have the following items so we can get the correct input from our users:

  • Input for the zip code or the city
  • Input for the country code
  • A Submit button

We are also going to add some conditional logic to our application. Instead of trying to parse the input, we are going to conditionally render a text or number input based on a checkbox to the left of our input. With these ideas, our WeatherInput.svelte file should look like the following:

<script>
import { zipcode } from './stores.js';
const api_key = '<your_api_key>'

let city = null;
let zip = null;
let country_code = null;

const submitData = function() {
fetch(`https://api.openweathermap.org/data/2.5/weather?q=${zipcode
? zip : city},${country_code}&appid=${api_key}`)
.then(res => res.json())
.then(final => console.log(final));
}
</script>
<style>
input:valid {
border: 1px solid #333;
}
input:invalid {
border: 1px solid #c71e19;
}
</style>
<div>
<input type="checkbox" bind:checked={$zipcode} />
{#if zipcode}
<input type="number" bind:value={zip} minLength="6" maxLength="10"
require />
{:else}
<input type="text" bind:value={city} required />
{/if}
<input type="text" bind:value={country_code} minLength="2"
maxLength="2" required />
<button on:click={submitData}>Check</button>
</div>

With this, we have the basic template for our input. First, we create a zipcode store to conditionally display a number or text input. Then, we create a couple of local variables that we will bind to our input values. The submitData function will submit everything once we are ready to get some type of response. Currently, we are just logging the output to the developer console.

For styling, we just added some basic styling for valid versus invalid inputs. Our template gives us a checkbox to turn on the zipcode feature or to turn it off. We then conditionally show the zipcode or the city textbox. Each of these textboxes has the built-in validation added to it. Next, we added another text field to get a country code from our users. Finally, we added a button that will go out and check for the data.

The brackets are heavily utilized in Svelte. One feature of input validation is regex based. The field is called a pattern. If we try to utilize brackets in here, it will cause the Svelte compiler to fail. Just be aware of this.

Before we get to the output, let's go ahead and add some labels to our input to make it easier for users to use. The following should do it:

//in the style tag
input {
margin-left: 10px;
}
label {
display: inline-block;
}
#cc input {
width: 3em;
}

For every input element, we have wrapped them in a label like so:

<label id="cc">Country Code<input type="text" bind:value={country_code} minLength="2" maxLength="2" required /></label>

With this, we have the basic user interface for our input element. Now, we need to have the fetch call actually output to something that can be available to our WeatherOutput element once we have made it. Instead of just passing this data out as props, let's create a custom store that implements a gather method. Inside of stores.js, we should have something that looks like the following:

function createWeather() {
const { subscribe, update } = writable({});
const api_key = '<your_api_key>';
return {
subscribe,
gather: (cc, _z, zip=null, city=null) => {
fetch(`https://api.openweathermap.org/data/2.5/weather?=${_z ?
zip : city},${cc}&appid=${api_key})
.then(res => res.json())
.then(final => update(() => { return {...final} }));
}
}
}

We have now moved the logic of getting the data into a store and we can now subscribe to this store to update ourselves. This will mean that we can make the WeatherOutput component subscribe to this for some basic output. The following code should be put into WeatherOtuput.svelte:

<script>
import { weather } from './stores.js';
</script>
<style>
</style>
<p>{JSON.stringify($weather)}</p>

All we are doing for now is putting the output of our weather into a paragraph element and stringifying it so we can read the output without looking at the console. We also need to update our App.svelte file and import the WeatherOutput component like so:

//inside the script tag
import WeatherOutput from './WeatherOutput.svelte'

//part of the template
<WeatherOutput></WeatherOutput>

If we now test our application, we should get some ugly-looking JSON, but we have now tied our two components through the store! Now, all we need to do is pretty up the output, and we have a fully functioning weather application! Change the styling and the template inside of WeatherOutput.svelte to the following:

<div>
{#if $weather.error}
<p>There was an error getting your data!</p>
{:else if $weather.data}
<dl>
<dt>Conditions</dt>
<dd>{$weather.weather}</dd>
<dt>Temperature</dt>
<dd>{$weather.temperature.current}</dd>
<dd>{$weather.temperature.min}</dd>
<dd>{$weather.temperature.max}</dd>
<dt>Humidity</dt>
<dd>{$weather.humidity}</dd>
<dt>Sunrise</dt>
<dd>{$weather.sunrise}</dd>
<dt>Sunset</dt>
<dd>{$weather.sunset}</dd>
<dt>Windspeed</dt>
<dd>{$weather.windspeed}</dd>
<dt>Direction</dt>
<dd>{$weather.direction}</dd>
</dl>
{:else}
<p>No city or zipcode has been submitted!</p>
{/if}
</div>

Finally, we should add a new control so our users can pick metric or imperial units for the output. Add the following to the WeatherInput.svelte:

<label>Metric?<input type="checkbox" bind:checked={$metric}</label>

We will also use a new metric store to the stores.js file that defaults to false. With all of this, we should have now have a functioning weather application! The only piece that we have left is to add the LocalStorage capabilities.

There are two types of storage that do similar things. They are LocalStorage and SessionStorage. The main difference is how long they will stay cached. LocalStorage stays until the user deletes the cache or the application developer decides to delete it. SessionStorage stays in the cache for the lifetime of the page. Once the user decides to leave the page, SessionStorage will clear out. Leaving the page means closing the tab or navigating away; it does not mean reloading the page or Chrome crashing and the user recovering the page. It is up to the designer which one to use.

Utilizing LocalStorage is quite easy. The object is held on the window in our case (if we were in a worker, it would be held on the global object). One thing to keep in mind is that when we utilize LocalStorage, it converts all values to strings, so we will need to convert complex objects if we want to store them.

To change our application, let's create a new component specifically for our drop-down. Let's call it Dropdown. First, create a Dropdown.svelte file. Next, add the following code to the file:

<script>
import { weather } from './stores.js';
import { onDestroy, onMount } from 'svelte';

export let type = "text";
export let name = "DEFAULT";
export let value = null;
export let required = true;
export let minLength = 0;
export let maxLength = 100000;
let active = false;
let inputs = [];
let el;

const unsubscribe = weather.subscribe(() => {
if(!inputs.includes(value) ) {
inputs = [...inputs, value];
localStorage.setItem(name, inputs);
}
value = '';
});
const active = function() {
active = true;
}
const deactivate = function(ev) {
if(!ev.path.includes(el) )
active = false;
}
const add = function(ev) {
value = ev.target.innerText;
active = false;
}
const remove = function(ev) {
const text = ev.target.parentNode.querySelector('span').innerText;
const data = localStorage.getItem(name).split(',');
data.splice(data.indexOf(text));
inputs = [...data];
localStorage.setItem(name, inputs);
}
onMount(() => {
const data = localStorage.getItem(name);
if( data === "" ) { inputs = []; }
else { inputs = [...data.split(',')]; }
});
onDestroy(() => {
unsubscribe();
});
</script>
<style>
input:valid {
border 1px solid #333;
}
input:invalid {
border 1px solid #c71e19;
}
div {
position : relative;
}
ul {
position : absolute;
top : 100%;
list-style-type : none;
background : white;
display : none;
}
li {
cursor : hand;
border-bottom : 1px solid black;
}
ul.active {
display : inline-block;
}
</style>
<svelte:window on:mousedown={deactivate} />
<div>
{#if type === "text"}
<input on:focus={activate} type="text" bind:value={value}
{minLength} {maxLength} {required} />
{:else}
<input on:focus={activate} type="number" bind:value={value}
{minLength} {maxLength} {required} />
{/if}
<ul class:active bind:this={el}>
{#each inputs as input }
<li><span on:click={add}>{input}</span> <button
on:click={remove}>&times;</button></li>
{/each}
</ul>
</div>

This is quite a bit of code, so let's break down what we have just done. First, we are taking our inputs and changing them to a dropdown component. We are also internalizing a lot of the state for this component. We open up various fields for the user to be able to customize the fields themselves. The main field that we need to make sure that we set is name. This is what we are using for the LocalStorage key to store our searches.

Next, we subscribe to the weather store. We do not use the actual data, but we do get the event so we can add the selection to the store if it is unique (a set could be used here instead of an array). We add some basic logic if we want to activate the drop-down if we are focused or if we have clicked outside of our drop-down. We also add some logic to the click event of the list element (we actually add it to the children of the list element) for putting the text into the drop-down or removing from our LocalStorage. Finally, we add behavior to the onMount and the onDestroy of our component. onMount will pull from localStorage and add this to our inputs list. The onDestroy just removes our subscription so we do not have a memory leak.

The rest of the styling and the templating should look familiar, except for the bind:this in the unordered list system. This allows us to bind a variable to the element itself. This allows us to deactivate our drop-down list if the element is not inside of the list of elements in the event path.

With this, make the following updates to the WeatherInput.svelte:

//inside the script tag
import Dropdown from './Dropdown.svelte';

//part of the template
{#if $zipcode}
<label>Zip<Dropdown name="zip" type="number" bind:value={zip}
minLength="6" maxLength="10"></Dropdown></label>
{:else}
<label>City<Dropdown name="city" bind:value={city}></Dropdown></label>
{/if}
<label>Country Code<Dropdown name="cc" bind:value={country_code}
minLength="2" maxLength="2"></Dropdown></label>

We have now created a semi-reusable dropdown component (we do rely on the weather store, so it really only works with our application) and have created something that looks like a single component.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.239.182