Search code examples
javascriptangularjsangularjs-directiveangularjs-scope

AngularJS ngRepeat within Directiving using a locally defined collection


I'm trying to figure out how to use a locally defined array of objects within a directive for use of an ngRepeat - I'm not using any controllers for these.

I've got a directive template setup like below

<div id="radioBtn" class="btn-group env-switcher">
        <a
            ng-repeat="env in environments" ng-click="setEnv(env)" ng-class="{'active': env.active}"
            href="javascript:void(0);" class="btn btn-info btn-sm" >

            <i class="fa {{env.icon}}" aria-hidden="true"></i>
            &nbsp;&nbsp;{{env.label}}

        </a>
    </div>

and I've got my directive code

App.directive('envSwitcher', [function(){
    return {
        restrict: 'C',
        link: function($scope, $elem, $attrs){

            $scope.activeEnvironment = 'unknown';

            $scope.environments = [
                {
                    label: 'Local',
                    icon: 'fa-hand-o-down',
                    value: 'local',
                    active: true
                },
                {
                    label: 'Staging',
                    icon: 'fa-hand-rock-o',
                    value: 'staging',
                    active: false
                },
                {
                    label: 'Live',
                    icon: 'fa-hand-o-up',
                    value: 'live',
                    active: false
                }
            ];

            $scope.setEnv = function(env){

                angular.forEach($scope.environments, function(model, key){

                    if( angular.equals(env, model) ){
                        model.active = true;
                        $scope.activeEnvironment = model.value;
                    }
                    else {
                        model.active = false;
                    }
                });
            }
        }
    }
}]);

However, when you have multiple of these on the page, the $scope bleeds out and updates them all. So if I run the setEnv function on Directive Instance 1 then Directive Instance 2 and 3 also change.

I'm aware there is a scope property which i've tried to set to scope: {} to isolate it, however then the ngRepeat doesn't show anything - im guessing because I've done something wrong.

How would I achieve the above? e.g Using a locally defined array of object, to use with ngRepeat within the directives template, without the $scope bleed?

Thanks


Solution

  • I'm assuming this is the current behaviour that you want to avoid:

    function envSwitcher() {
        return {
            restrict: 'C',
            link: function($scope, $elem, $attrs){
    
                $scope.activeEnvironment = 'unknown';
    
                $scope.environments = [
                    {
                        label: 'Local',
                        icon: 'fa-hand-o-down',
                        value: 'local',
                        active: true
                    },
                    {
                        label: 'Staging',
                        icon: 'fa-hand-rock-o',
                        value: 'staging',
                        active: false
                    },
                    {
                        label: 'Live',
                        icon: 'fa-hand-o-up',
                        value: 'live',
                        active: false
                    }
                ];
    
                $scope.setEnv = function(env){
                    angular.forEach($scope.environments, function(model, key){
    
                        if( angular.equals(env, model) ){
                            model.active = true;
                            $scope.activeEnvironment = model.value;
                        }
                        else {
                            model.active = false;
                        }
                    });
                }
            }
        }
    }
    
    angular.module('myApp', []);
    angular
        .module('myApp')
        .directive('envSwitcher', envSwitcher);
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
    <div ng-app="myApp">
      <div id="radioBtn" class="btn-group env-switcher">
            <a ng-repeat="env in environments"
              ng-click="setEnv(env)"
              ng-class="{'active': env.active}"
              href=""
              class="btn btn-info btn-sm">
                {{env.label}}
            </a> <span>Active env: {{activeEnvironment}}</span>
      </div>
      <div id="radioBtn" class="btn-group env-switcher">
            <a ng-repeat="env in environments"
              ng-click="setEnv(env)"
              ng-class="{'active': env.active}"
              href=""
              class="btn btn-info btn-sm">
                {{env.label}}
            </a> <span>Active env: {{activeEnvironment}}</span>
      </div>
    </div>

    You share a scope between your directives. If you want them to be independent, you have to put the a link inside a template of your directive which will allow isolating the scope.

    Here I use a component-like directive using controller instead of link because an independent, statefull directive should be a component. (and I don't know which angular version you are using or if directives are imposed on you)

    I also restricted the directive to attribute because using class directives is not best practice.

    function envSwitcher() {
      return {
        restrict: 'A',
        scope: {},
        template: `
          <a ng-repeat="env in ec.environments"
            ng-click="ec.setEnv(env)"
            ng-class="{'active': env.active}"
            href=""
            class="btn btn-info btn-sm">
              {{env.label}}
          </a> <span>Active env: {{ec.activeEnvironment}}</span>
        `,
        controller: envController,
        controllerAs: 'ec'
      }
    }
    
    function envController() {
      var vm = this;
      this.activeEnvironment = 'unknown';
      this.setEnv = setEnv;
      this.environments = [
        {
          label: 'Local',
          icon: 'fa-hand-o-down',
          value: 'local',
          active: true
        },
        {
          label: 'Staging',
          icon: 'fa-hand-rock-o',
          value: 'staging',
          active: false
        },
        {
          label: 'Live',
          icon: 'fa-hand-o-up',
          value: 'live',
          active: false
        }
      ];
    
      function setEnv(env){
        angular.forEach(vm.environments, function(model, key){
          if(angular.equals(env, model)){
            model.active = true;
            vm.activeEnvironment = model.value;
          }
          else {
            model.active = false;
          }
        });
      }
    }
    
        angular.module('myApp', []);
        angular
            .module('myApp')
            .directive('envSwitcher', envSwitcher)
            .controller('envController', envController);
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
    <div ng-app="myApp">
      <div id="radioBtn" env-switcher class="btn-group"></div>
      <div id="radioBtn" env-switcher class="btn-group"></div>
    </div>

    EDIT:

    To make your original code work with minimal changes, you have to copy the content of the div (a href...) in the template of the directive and then you can add the isolate scope.

    function envSwitcher() {
        return {
            restrict: 'C',
            scope: {},
            template: `
            <a ng-repeat="env in environments"
              ng-click="setEnv(env)"
              ng-class="{'active': env.active}"
              href=""
              class="btn btn-info btn-sm">
                {{env.label}}
            </a> <span>Active env: {{activeEnvironment}}</span>
    `,
            link: function($scope, $elem, $attrs){
    
                $scope.activeEnvironment = 'unknown';
    
                $scope.environments = [
                    {
                        label: 'Local',
                        icon: 'fa-hand-o-down',
                        value: 'local',
                        active: true
                    },
                    {
                        label: 'Staging',
                        icon: 'fa-hand-rock-o',
                        value: 'staging',
                        active: false
                    },
                    {
                        label: 'Live',
                        icon: 'fa-hand-o-up',
                        value: 'live',
                        active: false
                    }
                ];
    
                $scope.setEnv = function(env){
                    angular.forEach($scope.environments, function(model, key){
    
                        if( angular.equals(env, model) ){
                            model.active = true;
                            $scope.activeEnvironment = model.value;
                        }
                        else {
                            model.active = false;
                        }
                    });
                }
            }
        }
    }
    
    angular.module('myApp', []);
    angular
        .module('myApp')
        .directive('envSwitcher', envSwitcher);
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
    <div ng-app="myApp">
      <div id="radioBtn" class="btn-group env-switcher"></div>
      <div id="radioBtn" class="btn-group env-switcher"></div>
    </div>