Search code examples
javascriptangularjsangularjs-directiveangularjs-scope

How to toggle data in one directive using an ngClick function in another?


I'm building a weather app using Angular 1.5.8 and need to give users the ability to toggle back and forth between imperial and metric measurements for the temperature and wind speed.

The toggle option and all weather information (fetched from an external API) are located in separate directives, but I've thought about moving the temp and wind speed data to the same directive as the toggle option, then using either $broadcast or $emit to display the data and conversions in the weather directive. Is that the best way to go about doing this? If not, what would be?

Directive where the toggle is located:

app.directive('topBar', topBar);

function topBar() {
    return {
        template: 
        '<div class="changeTemp" ng-click="vm.changeTempUnit()">' +
            '<span ng-class="vm.fahrClass">&deg;F</span>' +
            '<span>/</span>' +
            '<span ng-class="vm.celsClass">&deg;C</span>' +
    '</div>',
        restrict: 'E',
        scope: {},
        controller: TopBarController,
        controllerAs: 'vm'
    };
}

function TopBarController() {
    var vm = this;

    vm.celsClass = 'unselected';
    vm.changeTempUnit = changeTempUnit;
    vm.fahrClass = 'selected';
    vm.temp;
    vm.windSpeed;

    function changeTempUnit() {
        if (vm.fahrClass === "selected") {
            vm.fahrClass = 'unselected'; //F unselected
            vm.celsClass = 'selected'; //C selected
            vm.temp = Math.round((vm.temp - 32) * 5 / 9); //Celsius
            vm.windSpeed = (vm.speed * 0.44704).toFixed(0); // M/S
        } else if (vm.celsClass === 'selected') {
            vm.celsClass = 'unselected'; //C unselected
            vm.fahrClass = 'selected'; //F selected
            vm.temp = Math.round(vm.temp * 1.8 + 32); //Fahren
            vm.windSpeed = (vm.speed / 0.44704).toFixed(0); //MPH
        }
    }
}

Directive where the weather is displayed

app.directive('weather', weather);

function weather() {
    return {
        template:
  '<div>' +
      'Temp: {{vm.temp}}&deg;' + '<br>' +
      'Wind Speed: {{vm.windSpeed}}' +
  '</div>',
        restrict: 'E',
        scope: {},
        controller: WeatherController,
        controllerAs: 'vm'
    };
}

WeatherController.$inject = ['weatherService'];

function WeatherController(weatherService) {
    var vm = this;

    vm.temp;
    vm.windSpeed;

    activate();

    function activate() {
        return weatherService.getWeather().then(function(data) {
                weatherInfo(data);
            });
    }

    function weatherInfo(data) {
        vm.temp = Math.round(data.main.temp); //Fahren
        vm.windSpeed = (data.wind.speed).toFixed(0); //MPH
    }
}

Plunker link


Solution

  • Use Components

    My first recommendation is to use the AngularJs 1.5+ component api. Components assume several of the directive definition object values you've already chosen.

    If your answer is yes to these questions then you should be using the component api instead.

    1. Does your directive have a template?
    2. Does your directive have an isolated scope?
    3. Does your directive have a controller?
    4. Is your directive restricted to elements?

    Converting your <top-bar> to a component would look like this

    app.component('topBar', {
        template: 
                '<div class="changeTemp" ng-click="$ctrl.changeTempUnit()">' +
                '<span ng-class="$ctrl.fahrClass">&deg;F</span>' +
                '<span>/</span>' +
                '<span ng-class="$ctrl.celsClass">&deg;C</span>' +
                '</div>',
        controller: TopBarController,
        bindings: {}
    });
    
    function TopBarController() {
    ...
    }
    

    Notice how the template uses $ctrl to refer to the controller instead of vm. With components, $ctrl is the default.

    Possible Solution

    emit and broadcast can be used and this is probably an ideal place to use them! but if you can avoid them, then don't rely on them.

    Here is one option

    Move computation to a service

    Topbar

    app.component('topBar', {
        template: 
                '<div class="changeTemp" ng-click="$ctrl.changeTempUnit()">' +
                '<span ng-class="$ctrl.fahrClass">&deg;F</span>' +
                '<span>/</span>' +
                '<span ng-class="$ctrl.celsClass">&deg;C</span>' +
                '</div>',
        controller: ['conversionService', TopBarController],
        bindings: {
    
        }
    })
    
    function TopBarController(conversionService) {
        var vm = this;
    
        vm.celsClass = 'unselected';
        vm.changeTempUnit = changeTempUnit;
        vm.fahrClass = 'selected';
    
        function changeTempUnit() {
            if (vm.fahrClass === "selected") {
                vm.fahrClass = 'unselected'; //F unselected
                vm.celsClass = 'selected'; //C selected
                conversionService.selectedUnit = conversionService.tempUnits.celsius;
            } else if (vm.celsClass === 'selected') {
                vm.celsClass = 'unselected'; //C unselected
                vm.fahrClass = 'selected'; //F selected
                conversionService.selectedUnit = conversionService.tempUnits.farhenheit;
            }
        } 
    }
    

    ConversionService

    app.service('conversionService', function() {
        var service = this;
    
        service.tempUnits = {
            farhenheit: 'farhenheit',
            celsius: 'celsius'
        };
    
        service.selectedUnit = 'farhenheit';
    
        service.convertTemperature = function(temp, tempUnit) {
            if (service.selectedUnit === tempUnit) {
                return temp;
            } else if (service.selectedUnit === service.tempUnits.farhenheiht) {
                return Math.round(temp * 1.8 + 32);
            } else if (service.selectedUnit === service.tempUnits.celsius) {
                return Math.round((temp - 32) * 5 / 9);
            } else {
                throw Error("Invalid unit");
            }
        } 
    });
    

    Weather

    app.component('weather', {
        template:
            '<div>' +
                'Temp: {{ $ctrl.getTemp() }}&deg;' + 
                '<br>' +
                'Wind Speed: {{ $ctrl.windSpeed }}' +
            '</div>',
        controller: ['conversionService', 'weatherService', WeatherController],
        bindings: {}
    });
    
    function WeatherController(conversionService, weatherService) {
        var ctrl = this;
    
        ctrl.temp;
        ctrl.windSpeed;
    
        ctrl.conversionService = conversionService;
    
        activate();
    
        function getTemp() {
            return ctrl.conversionService.convertTemperature(ctrl.temp, ctrl.conversionService.tempUnits.farhenheit);
        }
    
        function activate() {
            return weatherService.getWeather()
                .then(weatherInfo);
        }
    
        function weatherInfo(data) {
            ctrl.temp = Math.round(data.main.temp); //Fahren
            ctrl.windSpeed = (data.wind.speed).toFixed(0); //MPH
        }
    }
    

    Updated version of your plunk

    Since Angular does dirty checking when directives like ng-click evaluate their bound expressions the template of <weather> will also be dirty checked and the expression

    {{ $ctrl.conversionService.convertTemperature($ctrl.temp, $ctrl.conversionService.tempUnits.farhenheit) }}
    

    will be evaluated.