Proyecto MeteoApp – Parte 3

Tercera (y última) parte de este proyecto donde estamos construyendo una app de predicción meteorológica desde cero. Si acabas de aterrizar aquí, puedes ver las partes #1 y #2 antes.

Añadiendo imágenes e iconos dinámicos

Donde ahora tenemos un placeholder con un hueco para una imagen, vamos a añadir una dependiendo de si es de día o de noche según la hora que sea en la ciudad que busquemos. Después de conseguir eso, vamos a mostrar un icono arriba del nombre de la ciudad que refleje el clima actual. Por ejemplo, un icono de un sol si está soleado. ?

Esto lo vamos a conseguir actualizando el atributo src de cada imagen (la imagen del día/noche y la imagen del icono) cada vez que el usuario haga una búsqueda de una ciudad. 

1. Creamos una etiqueta HTML para la imagen del icono en el index.html.

            <div class="icon bg-light mx-auto text-center">
                <!-- icon will go here -->
                <img src="" alt="">
            </div>

Podemos deshacernos ya de ese comentario. ?

2. Obtenemos una referencia de cada imagen en nuestro archivo app.js. 

const timeImg = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

3. Hacemos un console.log de data dentro de la función updateUI, para así poder ver en la consola las propiedades de data y que nos resulte más fácil trabajar con ellas. Si vas a la consola, verás que la propiedad weather tiene una propiedad llamada WeatherIconcuyo valor es un número. Ese número está relacionado con una imagen (un icono), que será distinto según el clima que haga.

Existen 44 iconos asociados a 44 condiciones climáticas distintas. ?

Dentro de weather también tenemos la propiedad IsDayTimeun boolean que nos indica si es de día o de noche. 

? Un inciso: En este proyecto voy explicando todo lo que voy aprendiendo sobre JavaScript y está inspirado, entre otros, en el curso de The Net Ninja (JavaScript from Novice to Ninja). Así que para los iconos de este proyecto, el profe fue tan majo de recopilarlos de la web http://adamwhitcroft.com/climacons/ y re-nombrarlos con nombres del 1 al 44. Los puedes encontrar aquí, en su repositorio de GitHub

4. Pon la carpeta img que encontrarás en ese repositorio dentro de tu proyecto Meteo App, al nivel de index.htm. Aunque esa carpeta también la encontrarás en mi GitHub cuando este proyecto esté acabado. ? 

Vamos a encargarnos en primer lugar de actualizar las fotos de día / noche según la hora que sea en la ciudad que busquemos. Esto lo vamos a hacer dentro de la función updateUI.

5. Creamos una variable llamada timeSrc y le damos el valor de null. Esa variable será el valor del atributo src de nuestra imagen de día / noche (<img>). Para saber qué imagen mostrar, utilizamos la propiedad IsDayTime y hacemos una comprobación.

? Fíjate que utilizamos let para definir la variable timeSrc porque vamos a reescribir su valor. Así, si es de día, timeSrc será igual a la imagen de día, y si es de noche, a la imagen de noche.

    // update day/night img and icon
    let timeSrc = null;
    if(weather.IsDayTime) {
        timeSrc = 'img/day.svg';
    } else {
        timeSrc = 'img/night.svg';
    }

6. Hecho esto, ya podemos utilizar esa variable para sustituir el valor del atributo src de la imagen de esta parte: ​​

            <img src="https://via.placeholder.com/400x300" class="time card-img-top">

 Utilizamos setAttribute para ello.

    timeImg.setAttribute('src', timeSrc);

¡Y ahí lo tenemos! ? ?

setAttribute en acción

Ahora vamos a pasar a la parte del icono.

7. Creamos una variable llamada iconSrc cuyo valor vamos a escribir en template strings. Usamos template strings porque dentro del valor pondremos una variable que será dinámica, pudiendo adquirir el valor de 1 a 44. El número correcto lo vamos a obtener de la propiedad WeatherIcon. 

Lo que estamos haciendo aquí es la construcción de un archivo svg. Hecha esa construcción, ya podemos usarla como valor del atributo src de nuestra variable icon.  

    const iconSrc = `img/icons/${weather.WeatherIcon}.svg`;
    icon.setAttribute('src', iconSrc);

¡Y ahora también aparece el icono! ¡Genial! Aunque está un pelín feo. ? Vamos a darle algo de estilo.

.icon {
    border-radius: 50%;
    position: relative;
    top: -3rem;
}

Y ahora sí, nuestro icono tiene mucha mejor pinta.

icono con CSS

Usando el Ternary operator para simplificar nuestro código

El ternary operator es una estructura de comprobación de información en JS, como el if / else statement. De hecho, es un if / else statement pero de manera reducida. Esta es su sintaxis:

sintaxis ternary operator

Vamos a usarlo sobre este bloque de código, que determina qué imagen mostrar según sea de día o de noche:

    let timeSrc = null;
    if(weather.IsDayTime) {
        timeSrc = 'img/day.svg';
    } else {
        timeSrc = 'img/night.svg';
    }

Así que ya podemos comentar ese código y sustituirlo por este otro:

    let timeSrc = weather.IsDayTime ? 'img/day.svg' : 'img/night.svg';

Mismo código, pero en una sola línea. ?

? NOTA: Para poder mostrar la app en GitHub pages, tuve que actualizar el protocolo (de http https), así que así lo verás en las API requests del archivo forecast.js.

Guardando la ciudad en local storage

 Por último, vamos a guardar la ciudad que el usuario busque para que, si recarga la página, se le siga mostrando el clima de esa ciudad. Esto lo vamos a realizar usando la local storage API con la que cuenta JS por defecto.

Para saber si el usuario ha hecho una búsqueda de una ciudad anteriormente, guardaremos su búsqueda en local storage y haremos una comprobación cada vez que vuelva a la app después de haberla cerrado o recargado la página.

Si efectivamente había hecho alguna búsqueda antes de cerrar/recargar el navegador, le mostraremos la última ciudad que buscó. Eso lo haremos actualizando la UI y haciendo una http request con esa ciudad. Para ello, vamos a trabajar sobre el archivo app.js.

1. El código lo vamos a desarrollar dentro de nuestro submit eventAhí, guardamos la variable city en local storage. Recuerda que esa variable alberga la ciudad que el usuario ha buscado ?. A la key del local storage la llamamos city también.

cityForm.addEventListener('submit', e => {
    // prevent page refresh
    e.preventDefault()

    // get input value (city)
    const city = cityForm.city.value.trim();
    cityForm.reset();

    // update UI with new city
    updateCity(city)
        .then(data => updateUI(data))
        // .then(data => console.log(data))
        .catch(err => console.log(err));

    // store latest city in local storage
    localStorage.setItem('city', city);
});

Con este código, JS guardará en local storage el último nombre que busque el usuario, y sólo el último, porque lo que estamos haciendo es sobrescribir el valor del item cada vez que el usuario busca una nueva ciudad. ?

Si buscas una ciudad y vas a las dev tools, en la pestaña Application deberías ver esa ciudad ya almacenada en local storage. ¡Bien! Puedes buscar otra y verás como se sobrescribe. Lo que ocurre es que, si recargas la página, aunque el valor siga estando en local storage, en nuestra UI aún no podemos ver esa ciudad. ?

Para resolver eso, haremos una comprobación la primera vez que el usuario entra en en la página. Lo que comprobaremos es si ya existe alguna ciudad guardada en local storage. En ese caso, cogeremos ese valor y haremos una http request con él, para que la UI se actualice. 

2. Todo esto lo vamos a hacer al final del app.js, en la raíz, fuera de cualquier función, porque queremos que se ejecute nada más cargue la página. Para comprobar si un item existe en local storage, debemos obtenerlo primero, usando getItem. 

if(localStorage.getItem('city')) {
    
}

Este if statement devolverá un string si el valor existe, y null en caso contrario. Si el item existe, llamamos a la función updateCity, porque ahí es donde se hace la http request. A esta función le pasamos como parámetro la ciudad guardada en local storage.

3. La función updateCity devuelve una promesa, así que espera que hagamos algo cuando se resuelva o sea rechazada. Por tanto, le añadimos el método then y le especificamos que actualice la UI, igual que hacíamos más arriba cuando llamábamos a esa misma función.

Le añadimos también el método catch para capturar cualquier error en caso de que la promesa sea rechazada.

if (localStorage.getItem('city')) {
    updateCity(localStorage.getItem('city'))
        .then(data => updateUI(data))
        .catch(err => console.log(err));
}

¡Y voilà! Ya podemos ver en la app la última ciudad que hemos buscado. ?

Refactorizando el código con clases de JavaScript

Vamos a aplicar técnicas de Programación Orientada a Objetos para refactorizar nuestro código y hacerlo más moderno.

Creando la Forecast class

Sabemos que todo el código del forecast.js es el responsable de hacer las APIs calls. Vamos a coger todo ese código y comprimirlo en una clase de JS (en inglés, JS class). 

1. Empezamos creando la clase Forecast y su constructor. El constructor tendrá tres propiedades: la API key, la base de la URI de getWeather y la base de la URI de getCity. No necesitamos pasarle ningún parámetro al constructor, porque las instances de la clase Forecast no van a tener ninguna propiedad exclusiva. Es decir, cada instance va a tener las mismas propiedades y valores que el constructor de la clase. 

class Forecast {
    constructor() {
        this.key = 'oUHBzQZjvQjyuNjWDFg6y0hm5zDCuX7r';
        this.weatherURI = `https://dataservice.accuweather.com/currentconditions/v1/${id}`;
        this.cityURI = 'https://dataservice.accuweather.com/locations/v1/cities/search';
    }
}

Lo siguiente que vamos a hacer es convertir las funciones getWeather getCity (del archivo forecast.js) y la función updateCity (del archivo app.js) en métodos de la clase Forecast.

2. Recuerda que updateCity es el primer método al que llamamos, así que vamos a encargarnos de ese método primero. Fíjate que es un método asíncrono y así debe permanecer. Para hacer eso en el método de una clase, solo tenemos que añadir la keyword "asyncdelante del método. 

Le pasamos una ciudad como parámetro, cortamos todo el código de updateCity y lo copiamos dentro del método que estamos construyendo en la clase.

    async updateCity(city) {
        const cityDetails = await getCity(city);
        const weather = await getWeather(cityDetails.Key);

        return { cityDetails, weather }
    }

3. Modificamos el valor de la variable cityDetails, porque hemos dicho que el método getCity se convertirá en un método de la clase. Por tanto, para referirnos a él debemos anteponerle la palabra this

Hacemos lo mismo con la variable weather y el método getWeather. 

    async updateCity(city) {
        const cityDetails = await this.getCity(city);
        const weather = await this.getWeather(cityDetails.Key);
        return { cityDetails, weather }
    }

Vamos a ocuparnos ahora de añadir el método getCity a la clase.

4. ​También es un método asíncrono así que le añadimos la palabra async al principio. Le pasamos una ciudad como parámetro. 

5. Cortamos todo el código de su interior y lo pegamos dentro del método getCity que ya forma parte de la clase.

    async getCity(city) {
        const base = 'https://dataservice.accuweather.com/locations/v1/cities/search';
        const query = `?apikey=${key}&q=${city}`;

        const response = await fetch(base + query);
        const data = await response.json();

        return data[0]
    }

6. No necesitamos definir otra vez la variable base, porque su contenido ya lo tenemos en el constructor (en el this.cityURI). Así que lo borramos. En cuanto a la variable query, la mantenemos, pero ahora key hace referencia a la propiedad key del Forecast object, así que debemos usar el this delante.

7. Dentro del response, sustituimos base por this.cityURI.

async getCity(city) {
        const query = `?apikey=${this.key}&q=${city}`;
        const response = await fetch(this.cityURI + query);
        const data = await response.json();
        return data[0]
    }

Lo siguiente sería crear el método getWeather en la clase. Vamos a ello. 

8. También es un método asíncrono así que le agregamos la palabra async. 

9. Le pasamos por parámetro el código de la ciudad que el usuario busque (un id). De nuevo, cortamos el código de dentro de la función y lo pegamos en el nuevo método de la clase. Tampoco necesitamos la variable base porque ya la tenemos definida, así que la borramos.

10. Igual que para el método getCity, sustituimos key por this.key en la variable query, base por this.weatherURI en la variable response. 

    async getWeather(id) {
        const query = `?apikey=${this.key}`;
        const response = await fetch(this.weatherURI + query);
        const data = await response.json();
        return data[0];
    }

Esto supondría que no le pasaríamos el parámetro id a ninguna parte de la función, porque el id lo tenemos definido como parte del valor de la propiedad this.weatherURI en el constructor. Así que vamos a cambiar eso.

11. Incluimos el parámetro id como parte de la query.

        const query = `${id}?apikey=${this.key}`;

12. Borramos el id del valor de la propiedad this.weatherURI. 

        this.weatherURI = `https://dataservice.accuweather.com/currentconditions/v1/`;

Ya podemos borrar todas las funciones que hemos dejado vacías + la variable key. Y con este toque ya hemos terminado con el archivo forecast.js. Ahora vamos a encargarnos del app.js, donde debemos crear una nueva instance del Forecast object. ?

Creando una instance del Forecast object

1. Ya en el archivo app.js, creamos una instance de nuestro Forecast object. Lo hacemos a continuación de todos nuestros querySelectors. Comentamos el resto de código que tenemos justo después, para que no nos moleste y podamos hacer una serie de pruebas. ?

Hacemos un console.log de nuestro objeto recién creado.

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const timeImg = document.querySelector('img.time');
const icon = document.querySelector('.icon img');
const forecast = new Forecast();

console.log(forecast);

// const updateUI = (data) =&gt; {

//     console.log(data);
//     // const cityDetails = data.cityDetails;
//     // const weather = data.weather;

//     // destructuring
//     const { cityDetails, weather } = data;

//     details.innerHTML = `
//         <h5 class="my-3">${cityDetails.EnglishName}</h5>
//         <div class="my-3">${weather.WeatherText}</div>
//         <div class="display-4 my-4">
//             <span>${Math.floor(weather.Temperature.Metric.Value)}</span>
//             <span>°C</span>
//         </div>
//     `;

//     // update day/night img and icon
//     const iconSrc = `img/icons/${weather.WeatherIcon}.svg`;
//     icon.setAttribute('src', iconSrc);

//     // ternary operator
//     let timeSrc = weather.IsDayTime ? 'img/day.svg' : 'img/night.svg';
//     timeImg.setAttribute('src', timeSrc);

//     // check if d-none is present
//     if (card.classList.contains('d-none')) {
//         card.classList.remove('d-none');
//     }
// };

// const updateCity = async (city) =&gt; {
    
// };

// cityForm.addEventListener('submit', e =&gt; {
//     // prevent page refresh
//     e.preventDefault()

//     // get input value (city)
//     const city = cityForm.city.value.trim();
//     cityForm.reset();

//     // update UI with new city
//     updateCity(city)
//         .then(data =&gt; updateUI(data))
//         // .then(data =&gt; console.log(data))
//         .catch(err =&gt; console.log(err));

//     // store latest city in local storage
//     localStorage.setItem('city', city);
// });

// if (localStorage.getItem('city')) {
//     updateCity(localStorage.getItem('city'))
//         .then(data =&gt; updateUI(data))
//         .catch(err =&gt; console.log(err));
// }

En la consola podrás comprobar que se imprime nuestro nuevo Forecast object, a cuyos métodos tenemos acceso a través su prototype

|   Elements    Console    Sources    Performance    Network    ...

Forecast {key: "oUHBzQZjvQjyuNjWDFg6y0hm5zDCuX7r", weatherURI: "https://dataservice.accuweather.com/currentconditions/v1/", cityURI: "https://dataservice.accuweather.com/locations/v1/cities/search"}
cityURI: "https://dataservice.accuweather.com/locations/v1/cities/search"
key: "oUHBzQZjvQjyuNjWDFg6y0hm5zDCuX7r"
weatherURI: "https://dataservice.accuweather.com/currentconditions/v1/"
__proto__:
    constructor: class Forecast
    getCity: ƒ async getCity(city)
    getWeather: ƒ async getWeather(id)
    updateCity: ƒ async updateCity(city)
        __proto__: Object

Ya podemos descomentar el código y deshacernos del console.log. Ahora, cada vez que llamemos a un método como updateCity, debemos llamarlo a través de la variable forecastya que ésta contiene la clase Forecast con el método updateCity.

cityForm.addEventListener('submit', e => {
    // prevent page refresh
    e.preventDefault()

    // get input value (city)
    const city = cityForm.city.value.trim();
    cityForm.reset();

    // update UI with new city
    forecast.updateCity(city)
        .then(data => updateUI(data))
        // .then(data => console.log(data))
        .catch(err => console.log(err));

    // store latest city in local storage
    localStorage.setItem('city', city);
});

if (localStorage.getItem('city')) {
    forecast.updateCity(localStorage.getItem('city'))
        .then(data => updateUI(data))
        .catch(err => console.log(err));
}

¡Y ya lo tenemos! Nuestra app funciona igual que antes, pero el código sobre el que se basa es más moderno. ?

THE END!

¡Y con esto terminamos nuestra Meteo App! Espero que hayas aprendido algo nuevo ?.  Si te queda alguna duda, ¡nos vemos en los comentarios!

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Otros artículos que pueden interesarte

Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Días del 160 al 203 – ¡Primer objetivo conseguido!
“A veces podemos pasarnos años sin vivir en absoluto, y de pronto toda nuestra vida se concentra en un solo[...]
Claves para entender Angular. Qué es y cómo se utiliza
Angular es un framework creado por Google que nos permite construir Single Page Applications (SPA, por sus siglas en inglés).Frameworks¿Pero qué es[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Esta web utiliza cookies para asegurar que se da la mejor experiencia al usuario. Si continúas utilizando este sitio se asume que estás de acuerdo. más información

Los ajustes de cookies en esta web están configurados para «permitir las cookies» y ofrecerte la mejor experiencia de navegación posible. Si sigues usando esta web sin cambiar tus ajustes de cookies o haces clic en «Aceptar», estarás dando tu consentimiento a esto.

Cerrar