Know Your Weather version2 - uses Salesforce Lightning and Salesforce Lightning Design System


I have updated the application to include a picklist which will allow the user to choose a city and view its weather report. The previous version auto-detects your location and displays the weather report of that location. This version will also perform the same set of operation on page load with an additional functionality that will allow you to change the city and view the weather report of that city.



Talking from a technical standpoint, below components have been added to achieve this:
  • ChooseCity.cmp - This component provides the user with a picklist to choose the city
  • ChooseCityController.js - JS controller for ChooseCity.cmp
  • ChooseCityHelper.js - JS helper for ChooseCity.cmp

Lets try to understand the functionality of each component:

ChooseCity.cmp

<aura:component controller="WeatherCtrl">
    
    <!-- Handler for init event which populates the Change city picklist -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <!-- Declaring that this compenent can fire an event of type c:WeatherResponseEvent -->
    <aura:registerEvent name="WeatherResponseEvent" type="c:WeatherResponseEvent"/>
    
    <!-- Including the LatLng component -->
    <c:LatLng />
    
    <!-- Change city picklist -->
    <ul class="slds-has-dividers--around-space">
        <li class="slds-item">
            <div class="slds-tile slds-tile--board">
               <ui:inputSelect label="Change city:   " class="dynamic" aura:id="InputSelectDynamic" change="{!c.updateWeather}"/>
            </div>
        </li>
    </ul>
    
</aura:component>

The component provides the user a picklist through which he/she can select a particular city and view its weather report. The component defines a handler for init event which populates the picklist with its set of values on page load. On page load, the init function present in ChooseCityController.js is called, which in turns call createCityList function present in ChooseCityHelper.js. This function creates the list of city and populates the picklist present in ChooseCity.cmp.
The component also handles the "change" event for the picklist. On change of a value present in picklist, updateWeather function present in ChooseCityController.js is called, which in turns call the makeCallout function present in ChooseCityHelper.js. The makeCallout function calls the same method which we used in v1 of this app to make the webservice callout. The getCalloutResponseContents method present in apex controller WeatherCtrl makes the webservice callout. The response is handled by the callback function. The callback function, in turn calls the fireEvent function present in the same helper class. The fireEvent function gets the application level event we defined in v1 of this app, sets its parameters and fires the event. This event will be handled by Weather.cmp

ChooseCityController.js

({
    doInit : function(component, event, helper) {
        helper.createCityList(component);
    },
    
    updateWeather : function(component, event, helper) {
        helper.makeCallout(component, helper);
    }
})

This is the standard JS controller for ChooseCity.cmp. The primary job of this controller is to delegate the calls to its helper function.


ChooseCityHelper.js

({
    createCityList : function(component) {
        var opts = [
            { class: "optionClass", label: "Ahmedabad", value: "Ahmedabad" },
            { class: "optionClass", label: "Amritsar", value: "Amritsar" },
            { class: "optionClass", label: "Bangalore", value: "Bangalore" },
            { class: "optionClass", label: "Bhopal", value: "Bhopal" },
            { class: "optionClass", label: "Chandigarh", value: "Chandigarh" },
            { class: "optionClass", label: "Chennai", value: "Chennai" },
            { class: "optionClass", label: "Delhi", value: "Delhi" },
            { class: "optionClass", label: "Gurgaon", value: "Gurgaon" },
            { class: "optionClass", label: "Hyderabad", value: "Hyderabad" },
            { class: "optionClass", label: "Indore", value: "Indore" },
            { class: "optionClass", label: "Kochi", value: "Kochi" },
            { class: "optionClass", label: "Jaipur", value: "Jaipur" },
            { class: "optionClass", label: "Kolkata", value: "Kolkata" },
            { class: "optionClass", label: "Mumbai", value: "Mumbai" },
            { class: "optionClass", label: "Nagpur", value: "Nagpur" },
            { class: "optionClass", label: "Noida", value: "Noida" },
            { class: "optionClass", label: "Patna", value: "Patna" },
            { class: "optionClass", label: "Pune", value: "Pune" },
            { class: "optionClass", label: "Vadodara", value: "Vadodara" },
            { class: "optionClass", label: "Varanasi", value: "Varanasi" },     
            { class: "optionClass", label: "Visakhapatnam", value: "Visakhapatnam" }
            
        ];
        component.find("InputSelectDynamic").set("v.options", opts);
    },
    
    makeCallout : function(component, helper){
        var selectedValue = component.find("InputSelectDynamic").get("v.value");
        var action = component.get("c.getCalloutResponseContents");
        action.setParams({
            "url": 'http://api.openweathermap.org/data/2.5/weather?q=' + selectedValue + ',in&appid=9b2719bd93477d05ad2575ccb81cb666&units=metric'
        });
        action.setCallback(this, function(response){
            var state = response.getState();
            if (component.isValid() && state === "SUCCESS") {
                helper.fireEvent(component,event,JSON.parse(response.getReturnValue()));
            }
        });
        $A.enqueueAction(action);
    },
    
    fireEvent : function(component, event, response){
    var weatherRespEvent = $A.get("e.c:WeatherResponseEvent");
    weatherRespEvent.setParams({
        "resp": response
    });
    weatherRespEvent.fire();
    }
})

This is the JS helper for ChooseCity.cmp. The first function named createCityList gets called on the page load and provide the values for Choose City picklist present in Weather component. Here we create an array with 3 parameters - class as optionClass (standard class used for defining a picklist), the label and value. We then find the picklist using the aura:id value present on picklist and set its options.
The makeCallout function is called on change of the picklist value. We first get the value of the picklist and then get a reference of the getCalloutResponseContents method present in Apex controller. We set the parameters and then define the callback function. The callback function gets the application level event, sets the parameter and fires the event. Wohoo, the response gets updated in the WeatherApp at a lightning speed.


Changes to existing components:

WeatherApp.app

This app now inclues the ChooseCity.cmp instead of LatLng event. The LatLng event is now included in the ChooseCity.cmp.

<aura:application access="GLOBAL" extends="force:slds">
    <!-- Include ChooseCity component -->
    <c:ChooseCity />
</aura:application>

Other components in the application: For explanation of these components, please refer the v1 version of this application.

latLngEvent.evt

<aura:event type="APPLICATION" access="global">
    
    <!-- Define an attribute of type Decimal to store the latitude -->
    <aura:attribute name="lat" type="Decimal"/>
    
    <!-- Define an attribute of type Decimal to store the longitude -->
    <aura:attribute name="lng" type="Decimal"/>
    
</aura:event>


LatLng.cmp

<aura:component >
    
    <!-- Handles the init event of the page and calls the doInit method present in JS controller -->
    <aura:handler name="init" action="{!c.doInit}" value="{!this}"/>
    
    <!-- Declaring that this compenent can fire an event of type c:latLngEvent -->
    <aura:registerEvent name="latLngEvent" type="c:latLngEvent"/>
    
    <!-- Including the weather component -->
    <c:Weather />
    
</aura:component>

LatLngController.js

({
    doInit : function(component, event, helper) {
        helper.getLatLng(component,event,helper);
    }
})

LatLngHelper.js

({
    getLatLng : function(component,event,helper) {
        var lat,lng;
        
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function(position) {
                lat =  position.coords.latitude;
                lng = position.coords.longitude;
                helper.fireEvent(component, event, lat, lng);
            });
        }
        
        
    },
    
    fireEvent : function(component, event, lat, lng){
    
    var latLngEvent = $A.get("e.c:latLngEvent");
    latLngEvent.setParams({
        "lat": lat,
        "lng": lng
    });
    latLngEvent.fire();
    }

})

Weather.cmp

<aura:component controller="WeatherCtrl">
    
    <!-- Handles the event of type c:latLngEvent -->
    <aura:handler event="c:latLngEvent" action="{!c.getWeatherDetails}"/>
    
    <!-- Handles the event of type c:WeatherResponseEvent -->
    <aura:handler event="c:WeatherResponseEvent" action="{!c.updateResponse}"/>
    
    <!-- Define an attribute of type WeatherResponse apex class -->
    <aura:attribute name="response" type="WeatherResponse"/>
    
    <!-- Use SLDS classes to style the components -->
    <div class="slds-box">
        <p><b><div class="slds-text-heading--large">Weather Report - <ui:outputText value="{!v.response.name}"/></div></b></p>
    </div><br/>
    
    <ul class="slds-has-dividers--around-space">
        <li class="slds-item">
            <div class="slds-tile slds-tile--board">
                <h3 class="slds-truncate" title="Co-ordinates">Co-ordinates</h3>
                <div class="slds-tile__detail slds-text-body--small">
                    <p class="slds-text-heading--medium">Longitude</p>
                    <p class="slds-truncate"><ui:outputNumber value="{!v.response.coord.lon}" format=".00"/></p>
                    <p class="slds-text-heading--medium">Latitude</p>
                    <p class="slds-truncate"><ui:outputNumber value="{!v.response.coord.lat}" format=".00"/></p>
                </div>
            </div>
        </li>
        <li class="slds-item">
            <div class="slds-tile slds-tile--board">
                <h3 class="slds-truncate" title="Co-ordinates">Temprature</h3>
                <div class="slds-tile__detail slds-text-body--small">
                    <p class="slds-text-heading--medium">Avg. Temprature</p>
                    <p class="slds-truncate"><ui:outputNumber value="{!v.response.main.temp}" format=".00"/></p>
                    <p class="slds-text-heading--medium">Max. Temprature</p>
                    <p class="slds-truncate"><ui:outputNumber value="{!v.response.main.temp_max}" format=".00"/></p>
                    <p class="slds-text-heading--medium">Min. Temprature</p>
                    <p class="slds-truncate"><ui:outputNumber value="{!v.response.main.temp_min}" format=".00"/></p>
                    <p class="slds-text-heading--medium">Humidity</p>
                    <p class="slds-truncate"><ui:outputNumber value="{!v.response.main.humidity/100}" format=".00%"/></p>
                </div>
            </div>
        </li>
        <li class="slds-item">
            <div class="slds-tile slds-tile--board">
                <h3 class="slds-truncate" title="Co-ordinates">Twilight</h3>
                <div class="slds-tile__detail slds-text-body--small">
                    <p class="slds-text-heading--medium">Sunrise</p>
                    <p class="slds-truncate"><ui:outputText value="{!v.response.sys.sunrise}"/></p>
                    <p class="slds-text-heading--medium">Sunset</p>
                    <p class="slds-truncate"><ui:outputText value="{!v.response.sys.sunset}"/></p>
                </div>
            </div>
        </li>
    </ul>
    
</aura:component>

WeatherController.js

({
    getWeatherDetails : function(component, event, helper) {
        helper.getResponse(component,event);
    },
    
    updateResponse : function(component, event, helper){
        console.log(event.getParam("resp"));
        component.set("v.response", event.getParam("resp"));
    }
    
})

WeatherHelper.js​

({
    getResponse : function(component,event){
        var action = component.get("c.getCalloutResponseContents");
        var lat = event.getParam("lat");
        var lng = event.getParam("lng");
        action.setParams({
            "url": 'http://api.openweathermap.org/data/2.5/weather?lat=' +lat+ '&lon=' +lng+ '&appid=9b2719bd93477d05ad2575ccb81cb666&units=metric'
        });
        action.setCallback(this, function(response){
            var state = response.getState();
            if (component.isValid() && state === "SUCCESS") {
                component.set("v.response", JSON.parse(response.getReturnValue()));
            }
        });
        $A.enqueueAction(action);
    }
    
})

WeatherCtrl.apxc

public class WeatherCtrl {
    
    // Pass in the endpoint to be used using the string url
    @AuraEnabled
    public static String getCalloutResponseContents(String url) {
        
        // Instantiate a new http object
        Http h = new Http();
        
        // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
        HttpRequest req = new HttpRequest();
        req.setEndpoint(url);
        req.setMethod('GET');
        
        // Send the request, and return a response
        HttpResponse res = h.send(req);
        return JSON.serialize(WeatherResponse.parse(res.getBody()));
    }
    
}

WeatherResponse.apxc

//
// Generated by JSON2Apex http://json2apex.herokuapp.com/
//
public class WeatherResponse{
    public static void consumeObject(JSONParser parser) {
        Integer depth = 0;
        do {
            JSONToken curr = parser.getCurrentToken();
            if (curr == JSONToken.START_OBJECT || 
                curr == JSONToken.START_ARRAY) {
                depth++;
            } else if (curr == JSONToken.END_OBJECT ||
                curr == JSONToken.END_ARRAY) {
                depth--;
            }
        } while (depth > 0 && parser.nextToken() != null);
    }
    public class Weather {
        public Integer id {get;set;} 
        public String main {get;set;} 
        public String description {get;set;} 
        public String icon {get;set;} 
        public Weather(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'id') {
                            id = parser.getIntegerValue();
                        } else if (text == 'main') {
                            main = parser.getText();
                        } else if (text == 'description') {
                            description = parser.getText();
                        } else if (text == 'icon') {
                            icon = parser.getText();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Weather consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    public class Coord {
        public Double lon {get;set;} 
        public Double lat {get;set;} 
        public Coord(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'lon') {
                            lon = parser.getDoubleValue();
                        } else if (text == 'lat') {
                            lat = parser.getDoubleValue();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Coord consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    public class Wind {
        public Double speed {get;set;} 
        public Double deg {get;set;} 
        public Wind(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'speed') {
                            speed = parser.getDoubleValue();
                        } else if (text == 'deg') {
                            deg = parser.getDoubleValue();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Wind consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    public class Rain {
        public Integer h3 {get;set;} 
        public Rain(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'h3') {
                            h3 = parser.getIntegerValue();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Rain consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    public class Clouds {
        public Integer all {get;set;} 
        public Clouds(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'all') {
                            all = parser.getIntegerValue();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Clouds consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    public Coord coord {get;set;} 
    public Sys sys {get;set;} 
    public List<Weather> weather {get;set;} 
    public Main main {get;set;} 
    public Wind wind {get;set;} 
    public Rain rain {get;set;} 
    public Clouds clouds {get;set;} 
    public Integer dt {get;set;} 
    public Integer id {get;set;} 
    public String name {get;set;} 
    public Integer cod {get;set;} 
    public WeatherResponse(JSONParser parser) {
        while (parser.nextToken() != JSONToken.END_OBJECT) {
            if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                String text = parser.getText();
                if (parser.nextToken() != JSONToken.VALUE_NULL) {
                    if (text == 'coord') {
                        coord = new Coord(parser);
                    } else if (text == 'sys') {
                        sys = new Sys(parser);
                    } else if (text == 'weather') {
                        weather = new List<Weather>();
                        while (parser.nextToken() != JSONToken.END_ARRAY) {
                            weather.add(new Weather(parser));
                        }
                    } else if (text == 'main') {
                        main = new Main(parser);
                    } else if (text == 'wind') {
                        wind = new Wind(parser);
                    } else if (text == 'rain') {
                        rain = new Rain(parser);
                    } else if (text == 'clouds') {
                        clouds = new Clouds(parser);
                    } else if (text == 'dt') {
                        dt = parser.getIntegerValue();
                    } else if (text == 'id') {
                        id = parser.getIntegerValue();
                    } else if (text == 'name') {
                        name = parser.getText();
                    } else if (text == 'cod') {
                        cod = parser.getIntegerValue();
                    } else {
                        System.debug(LoggingLevel.WARN, 'Root consuming unrecognized property: '+text);
                        consumeObject(parser);
                    }
                }
            }
        }
    }
    
    public class Sys {
        public String country {get;set;} 
        public String sunrise {get;set;} 
        public String sunset {get;set;} 
        public Sys(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'country') {
                            country = parser.getText();
                        } else if (text == 'sunrise') {
                            Integer unixTime = parser.getIntegerValue();
                            DateTime dateInstance = datetime.newInstanceGmt(1970, 1, 1, 0, 0, 0);
                            sunrise = dateInstance.addSeconds(unixTime).format();
                        } else if (text == 'sunset') {
                            Integer unixTime = parser.getIntegerValue();
                            DateTime dateInstance = datetime.newInstanceGmt(1970, 1, 1, 0, 0, 0);
                            sunset = dateInstance.addSeconds(unixTime).format();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Sys consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    public class Main {
        public Double temp {get;set;} 
        public Integer humidity {get;set;} 
        public Integer pressure {get;set;} 
        public Double temp_min {get;set;} 
        public Double temp_max {get;set;} 
        public Main(JSONParser parser) {
            while (parser.nextToken() != JSONToken.END_OBJECT) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                    String text = parser.getText();
                    if (parser.nextToken() != JSONToken.VALUE_NULL) {
                        if (text == 'temp') {
                            temp = parser.getDoubleValue();
                        } else if (text == 'humidity') {
                            humidity = parser.getIntegerValue();
                        } else if (text == 'pressure') {
                            pressure = parser.getIntegerValue();
                        } else if (text == 'temp_min') {
                            temp_min = parser.getDoubleValue();
                        } else if (text == 'temp_max') {
                            temp_max = parser.getDoubleValue();
                        } else {
                            System.debug(LoggingLevel.WARN, 'Main consuming unrecognized property: '+text);
                            consumeObject(parser);
                        }
                    }
                }
            }
        }
    }
    
    
    public static WeatherResponse parse(String json) {
        return new WeatherResponse(System.JSON.createParser(json));
    }
}

Comments

Popular posts from this blog

Salesforce Lightning: Countdown timer

Salesforce Hacks: System.LimitException: Too many queueable jobs added to the queue: 2

Building an Org Role Hierarchy component in LWC