Project 1 – Building a Weather App in Salesforce: Using the Weatherbit.io API to show Weather data in a Lightning Web Component

Posted by

Hello readers! Often times, one of my favorite things to do on a Sunday is think up a small little project that I think would be cool to build and well…build it! I thought it would be cool to work with web service callouts from a Lightning Web Component and show some meaningful information into a readable UI and be able to see at any given moment differences between weather data across multiple cities. Here is the description I made up of the problem we are trying to solve:

Shipper Enterprises wants to know more information about weather data in areas they are shipping to at a glance so that they can drive notifications and messaging around any delays or potential issues with delivering a package. They currently have a monthly subscription with the Weatherbit.io API to callout and retrieve various properties of weather data in the cities they care about. They have asked you to build a POC on what that could look like with some very simple weather attributes such as temperature, wind speed and current level of precipitation. Let’s get started!

I went to Weatherbit.io, signed up for a free version, was provisioned my key and thats all I needed to get started. First, I created a Named Credential in my Salesforce Org to store the URL I would need to perform the callout and add the necessary URL parameters to.

Next, I started writing out the class. Now, this is what the sample JSON output of data looks like when we retrieve information about a given city:

{  
    "data":[  
       {  
          "wind_cdir":"NE",
          "rh":59,
          "pod":"d",
          "lon":"-78.63861",
          "pres":1006.6,
          "timezone":"America\/New_York",
          "ob_time":"2017-08-28 16:45",
          "country_code":"US",
          "clouds":75,
          "vis":10,
          "wind_spd":6.17,
          "wind_cdir_full":"northeast",
          "app_temp":24.25,
          "state_code":"NC",
          "ts":1503936000,
          "h_angle":0,
          "dewpt":15.65,
          "weather":{  
             "icon":"c03d",
             "code":"803",
             "description":"Broken clouds"
          },
          "uv":2,
          "aqi":45,
          "station":"CMVN7",
          "wind_dir":50,
          "elev_angle":63,
          "datetime":"2017-08-28:17",
          "precip":0,
          "ghi":444.4,
          "dni":500,
          "dhi":120,
          "solar_rad":350,
          "city_name":"Raleigh",
          "sunrise":"10:44",
          "sunset":"23:47",
          "temp":24.19,
          "lat":"35.7721",
          "slp":1022.2
       }
    ],
    "count":1
 }

I don’t want all of this data just yet, so I want to figure out a way to parse what I need, store it into a custom wrapper object and then send it on its way to the LWC to show. Here is the class performing the callout:

global class WebServiceLWC {

    @AuraEnabled (cacheable=true)
    global static WeatherData performCallout(String location) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:WeatherAPI?city=' + location + '&key=--key--');
        req.setMethod('GET');
        Http http = new Http();
        HTTPResponse res = http.send(req);
        JSONParser parser = JSON.createParser(res.getBody());

        WeatherData weather = new WeatherData();

        while (parser.nextToken() != null) {
            if(parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                parser.nextValue();
                if (parser.getCurrentName() == 'temp') {
                    weather.cityTemp = Decimal.valueOf(parser.getText());
                } else if (parser.getCurrentName() == 'city_name') {
                    weather.cityName = parser.getText();
                } else if (parser.getCurrentName() == 'state_code') {
                    weather.state = parser.getText();
                } else if (parser.getCurrentName() == 'timezone') {
                    weather.cityTimeZone = parser.getText();
                } else if (parser.getCurrentName() == 'wind_spd') {
                    weather.cityWindSpeed = Decimal.valueOf(parser.getText());
                } else if (parser.getCurrentName() == 'lat') {
                    weather.cityLat = parser.getText();
                } else if (parser.getCurrentName() == 'lon') {
                    weather.cityLong = parser.getText();
                } else if (parser.getCurrentName() == 'precip') {
                    weather.cityPrecip = Decimal.valueOf(parser.getText());
                }
            }
        }
        return weather;
    }
    

    global class WeatherData {
        @AuraEnabled public String cityName;
        @AuraEnabled public String cityTimeZone;
        @AuraEnabled public Decimal cityTemp;
        @AuraEnabled public String state;
        @AuraEnabled public Decimal cityWindSpeed;
        @AuraEnabled public String cityLat;
        @AuraEnabled public String cityLong;
        @AuraEnabled public Decimal cityPrecip;
    }
}

On a side note, there are other ways to parse JSON. One is to use the JSON.deserializeUntyped(‘string’); that deserializes the JSON into a Map<String, Object> and then use a traditional casting strategy to get the value out: String val = (String) map.get(‘something’). The other is to utilize JSON2Apex to copy and paste your JSON and let the tool construct your wrapper class with all of the properties you need to set. The method above is fast and gets the job done for what we are trying to accomplish. Now that we have our properties constructed into a wrapper object, we want to make sure to not only declare the @AuraEnabled annotation on our retrieval method but also on each property that we want to expose to the Lightning Web Component. We also want to mark (cacheable=true) so that the client can store our web service callout results and not have to go back and fetch them again, making it more performant.

After we have tested our callout and made sure that we are constructing the correct wrapper properties, its time to write a little JavaScript to parse the data and assign it to our tracked properties. Here is the code:

import { LightningElement, track, wire } from 'lwc';
import { loadStyle, loadScript } from 'lightning/platformResourceLoader';
import performCallout from '@salesforce/apex/WebServiceLWC.performCallout';
import weather from '@salesforce/resourceUrl/weather';

export default class WeatherDataLWC extends LightningElement {

    @track lat;
    @track long;

    @track mapMarkers = [];
    zoomLevel = 10;
    @track result;
    @track value;

    connectedCallback() {
        performCallout({location: 'Denver,CO'}).then(data => {
            this.mapMarkers = [{
                location: {
                    Latitude: data['cityLat'],
                    Longitude: data['cityLong']
                },
                title: data['cityName'] + ', ' + data['state'],
            }];
            this.result = data;
        }).catch(err => console.log(err));
        loadStyle(this, weather).then(result => {
            console.log('what is the result?' , result);
        });
    }

    get getCityName() {
        if (this.result) {
            return this.result.cityName + ' Information';
        } else {
            return '---'
        }
    }

    get getConvertedTemp() {
        if (this.result) {
            return Math.round((this.result.cityTemp * (9/5)) + 32) + ' deg';
        } else {
            return '--'
        }
    }

    get getCurrentWindSpeed() {
        if (this.result) {
            return this.result.cityWindSpeed + ' mph';
        } else {
            return '--'
        }
    }

    get getCurrentPrecip() {
        if (this.result) {
            return this.result.cityPrecip + " inches"
        } else {
            return '--'
        }
    }

    get options() {
        return [
            { label: 'Arvada, CO', value: 'Arvada,CO' },
            { label: 'Austin, TX', value: 'Austin,TX' },
            { label: 'Sacramento, CA', value: 'Sacramento,CA' },
            { label: 'Raleigh, NC', value: 'Raleigh,NC' }
        ];
    }

    handleChange(event) {
        this.value = event.detail.value;
        performCallout({location: this.value}).then(data => {
            this.mapMarkers = [{
                location: {
                    Latitude: data['cityLat'],
                    Longitude: data['cityLong']
                },
                title: data['cityName'] + ', ' + data['state'],
            }];
            this.result = data;
        }).catch(err => console.log(err));
    }
}

I have highlighted the important stuff above. First, let’s talk a little about styling. We know that we have a .css file that we can leverage and utilize but what if we we want to make an enhancement to a Lightning Component or Visualforce page with this same styling? It would be unfortunate to have this code live in multiple places, so I wanted to show how we can leverage a static resource to apply styling rather than always putting it into the css file of the bundle. The local css file serves well for styling only applicable to that component, however for brand styling or multi use components, static resources are great! If you notice on line 2, we are using the PlatformResourceLoader module to load in the function loadStyle(). This will allow us to then perform the necessary functions of getting our script resource. On line 4, we are doing exactly that by importing our weather resource url. On line 27, asynchronously of the perform callout, we go and retrieve the script by calling loadStyle() and passing in our imported weather css resource. Now we have the ability to utilize it in our markup! Here is a simple css file that was created to style this component and load in as a static resource.

.color-blue {
    background-color: #08a7da;
    height: 100px;
    color: white;
    font-family: Tahoma;
    font-size: 45px;
    font-weight: 500;
    border-radius: 25px;
}

.color-grey {
    background-color: grey;
    height: 100px;
    color: white;
    font-family: Tahoma;
    font-size: 45px;
    font-weight: 500;
    border-radius: 25px;
}

.color-green {
    background-color: #59e0cd;
    height: 100px;
    color: white;
    font-family: Tahoma;
    font-size: 45px;
    font-weight: 500;
    border-radius: 25px;
}

As were building this component, one of the devs points out that we have access to the Lat and Long fields from the API and it might be nice to show a visual of the location as we review the current weather data. As you see above, we are using the lat and long to construct the lightning-map and pass in the lat and long collected from the API to show the location. Heres what it looks like in HTML (side note: we can also designate zoom level on load which is great!):

<template>
    <lightning-card title={getCityName}>
        <lightning-layout>
            <lightning-layout-item size="6" medium-device-size="4" padding="around-small">
                <lightning-combobox name="cities"
                    label="Cities"
                    value={value}
                    placeholder="--Select--"
                    options={options}
                    onchange={handleChange} >
                </lightning-combobox>
            </lightning-layout-item>
        </lightning-layout>
        <lightning-layout>
                <lightning-layout-item size="3" medium-device-size="4" padding="around-small">
                    <lightning-card icon-name="standard:topic" title="Current Temperature">
                        <div class="slds-align_absolute-center color-blue">
                            <template if:true={result}>
                                {getConvertedTemp}
                            </template>
                        </div>
                    </lightning-card>
                </lightning-layout-item>
                <lightning-layout-item size="3" medium-device-size="4" padding="around-small">
                    <lightning-card icon-name="utility:flow" title="Current Wind Speed">
                        <div class="slds-align_absolute-center color-grey">
                            <template if:true={result}>
                                {getCurrentWindSpeed}
                            </template>
                        </div>
                    </lightning-card>
                </lightning-layout-item>
                <lightning-layout-item size="3" medium-device-size="4" padding="around-small">
                    <lightning-card icon-name="standard:calibration" title="Precipitation">
                        <div class="slds-align_absolute-center color-green">
                            <template if:true={result}>
                                {getCurrentPrecip}
                            </template>
                        </div>
                    </lightning-card>
                </lightning-layout-item>
            </lightning-layout>
        <lightning-map
            map-markers={mapMarkers}
            zoom-level={zoomLevel}>
        </lightning-map>
    </lightning-card>
</template>

As you can see from the highlighted line 17 of the JS file, we want to parse the returned wrapper data and dynamically change the lat and long attributes of the mapMarkers object. This lets us continually change the map location on selection of different cities. Lastly, on line 17, we set our data retrieval to a tracked property so we can properly set up getter methods to show data if the result is set, otherwise we show the default property.

The overall purpose of this exercise is to illustrate how we utilize a web service callout to show data inside of a Lightning Web Component. Below you can see a screen shot of the component and a screen share that illustrates its functionality and performance. As always, if you have any questions or concerns, please feel free to reach out. Happy coding!

7 comments

  1. Taylor –
    Thanks for this example. I’m pretty proficient in Apex, but LWC is another thing. It’s helpful to see some examples like this. I just built this component and it works great. Thanks!
    MCR

    Like

  2. Was trying to replicate this code with AirNow website to get the air quality data and it doesn’t work. Can someone help?

    Like

  3. I’m having trouble with that… Can you explain in more detail what this action is and what we are referring to here?
    import weather from ‘@salesforce/resourceUrl/weather’;

    Like

  4. Hi Taylor,

    Thanks so much for posting this project. I’m pretty novice in development, so I am stuck in getting the LWC to populate with the actual data. Do I need to create a custom object (i.e. WeatherData__c) with the custom fields (i.e. cityTemp__c, cityName__c, etc) and update the WebServiceLWC class to hold those values?

    on line 15 there is a reference to JSONToken.FIELD_NAME – does the .FIELD_NAME need to be updated/replaced with something specific to my dev org?

    Again thanks so much for posting this and sorry if these are rookie questions.

    Like

Leave a comment