3

I'm trying to figure out more maintainable ways of composing Lightning components.

I'm currently looking at the helper on Display Weather Alerts in Lightning Experience, trying to break it down into a collection of smaller methods.

Currently my code looks like this:

({
    getCountryAlerts : function(component) {
        this.getFromServer(component, 'getWeatherAlerts', this.displayWeatherAlerts);
    },
    getFromServer: function(component, serverMethod, callback) {
        component.set('v.spinnerWaiting', true);
        var action = component.get('c.' + serverMethod);
        action.setParams({
            country: component.get('v.countrySelected')
        });
        action.setCallback(this, this.onResponse(component, callback));
        $A.enqueueAction(action);
    },
    displayWeatherAlerts: function(component, responseValue) {
        var alertsFromJson = responseValue.alerts;
        if (!alertsFromJson)  {
            this.toastMissingAlerts();
            return;
        }

        var map = component.get('v.map');
        this.removeOldMarkers(component, map);
        this.addNewMarkers(component, alertsFromJson, map);
        this.panToCountry(component, map);
    },
    removeOldMarkers: function(component, map) {
        var oldAlerts = component.get('v.alerts');
        for (var i = 0; i < oldAlerts.length; i++) {
            map.removeLayer(oldAlerts[i]);
        }
    }, 
    addNewMarkers: function(component, alertsFromJson, map) {
        var newAlerts = [];
        for (var i = 0; i < alertsFromJson.length; i++) {
            var newAlert = alertsFromJson[i];
            var marker = L.marker([newAlert.lat, newAlert.lon])
                .addTo(map)
                .bindPopup(newAlert.event_desc);
            newAlerts.push(marker);
        }
        component.set('v.alerts', newAlerts);
    },
    panToCountry: function(component, map) {
        var targetCountry = component.get('v.countrySelected')
        var countryOptions = component.get('v.countryOptions');
        var panToCountry = countryOptions(this.findTargetCountry(targetCountry));
        map.panTo(new L.LatLng(panToCountry.lat, panToCountry.lon));
    },
    findTargetCountry: function(targetCountry) {
        return function(optionCountry) {
            return optionCountry === targetCountry;
        };
    },
    onResponse: function(component, callback) {
        var self = this;
        return function(response) {
            if (response.getState() === 'ERROR') {
                self.onError(response.getError());
            }
            else {
                callback(component, JSON.parse(response.getReturnValue()));
            }
            component.set('v.spinnerWaiting', false);
        };
    },
    onError: function(errors) {
        var errorMessage = errors && errors[0] && errors[0].message;
        if (errorMessage) {
            alert('Error message: ' + errorMessage);
        }
        else {
            console.error('Unknown error');
        }
    },
    toastMissingAlerts: function() {
        var toastEvent = $A.get('e.force:showToast');
        toastEvent.setParams({
            message: 'There are no weather alerts for the selected country available. Lucky them!',
            title: 'No weather alerts!',
            type: 'success'
        });
        toastEvent.fire();
    }        
})

But this doesn't render and instead I get a nasty error telling me:

Uncaught Error in $A.getCallback() [this.removeOldMarkers is not a function] Callback failed: apex://IBM_WeatherAuraCtrl/ACTION$getWeatherAlerts

I'm guessing I need to put $A.getCallback() in one or more places to get this to work, but I'm not sure where... I've tried a multiple points without success.

Adding console.log('this: ', this) within displayWeatherAlerts() methods suggests that at this point, for some reason, this is now referring to window rather than helper... :-/

Any ideas?

  • 1
    It looks like it's actually working (getting to the displayWeatherAlerts method) but your response value does not contain an alerts property. Could this be it? What happens if you do a console.log(responseValue);? – Sebastian Kessel Aug 17 '17 at 16:29
  • Yes, just find out what's going on with the response - use your browser debugger or console log the result - something is probably wrong with your apex class and it doesn't have an alerts param. The nice thing about static controller classes is that you can also use the developer console to anonymous execute the method to see what it's output is if you find that easier. – Caspar Harmer Aug 17 '17 at 16:42
  • 1
    My guess is that the function returned by your onResponse function is jumping out of the Aura context. One simple thing you might try is to haveonResponse return $A.getCallback(...) with your anonymous function inside instead of directly returning the anonymous function. – dsharrison Aug 17 '17 at 21:16
  • 1
    As a general comment to addressing "callback hell" in Lightning Components, also check out Promise design pattern, developer.salesforce.com/docs/atlas.en-us.lightning.meta/… – Doug Ayers Aug 18 '17 at 0:40
  • 1
    @DougAyers, great idea! ... I actually asked about using promises with lightning half a year ago, but then haven't been using JavaScript much and forgot about them. – Brian Kessler Aug 18 '17 at 5:36
2

With thanks to @dsharrison, after working out a few other small bugs, I finally have the helper working as expected:

({
    getCountryAlerts : function(component) {
        this.getFromServer(component, 'getWeatherAlerts', this.displayWeatherAlerts);
    },
    getFromServer: function(component, serverMethod, callback) {
        component.set('v.spinnerWaiting', true);
        var action = component.get('c.' + serverMethod);
        action.setParams({
            country: component.get('v.countrySelected')
        });
        action.setCallback(this, this.onResponse(component, callback));
        $A.enqueueAction(action);
    },
    displayWeatherAlerts: function(component, responseValue) {
        var alertsFromJson = responseValue.alerts;
        if (!alertsFromJson)  {
            this.toastMissingAlerts();
            return;
        }

        var map = component.get('v.map');
        this.removeOldMarkers(component, map);
        this.addNewMarkers(component, alertsFromJson, map);
        this.panToCountry(component, map);
    },
    removeOldMarkers: function(component, map) {
        var oldAlerts = component.get('v.alerts');
        for (var i = 0; i < oldAlerts.length; i++) {
            map.removeLayer(oldAlerts[i]);
        }
    }, 
    addNewMarkers: function(component, alertsFromJson, map) {
        var newAlerts = [];
        for (var i = 0; i < alertsFromJson.length; i++) {
            var newAlert = alertsFromJson[i];
            var marker = L.marker([newAlert.lat, newAlert.lon])
                .addTo(map)
                .bindPopup(newAlert.event_desc);
            newAlerts.push(marker);
        }
        component.set('v.alerts', newAlerts);
    },
    panToCountry: function(component, map) {
        var targetCountry = component.get('v.countrySelected')
        var countryOptions = component.get('v.countryOptions');
        var panToCountry = countryOptions.find(helper.findTargetCountry(targetCountry));
        map.panTo(new L.LatLng(panToCountry.lat, panToCountry.lon));
    },
    findTargetCountry: function(targetCountry) {
        return function(optionCountry) {
            var isMatch = optionCountry.value === targetCountry; 
            return isMatch;
        };
    },
    onResponse: function(component, callback) {
        var self = this;
        return $A.getCallback(function(response) {
            if (response.getState() === 'ERROR') {
                self.onError(response.getError());
            }
            else {
                callback(component, JSON.parse(response.getReturnValue()));
            }
            component.set('v.spinnerWaiting', false);
        });
    },
    onError: function(errors) {
        var errorMessage = errors && errors[0] && errors[0].message;
        if (errorMessage) {
            alert('Error message: ' + errorMessage);
        }
        else {
            console.error('Unknown error');
        }
    },
    toastMissingAlerts: function() {
        var toastEvent = $A.get('e.force:showToast');
        toastEvent.setParams({
            message: 'There are no weather alerts for the selected country available. Lucky them!',
            title: 'No weather alerts!',
            type: 'success'
        });
        toastEvent.fire();
    }        
})

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.