7

I've a template that is appendend many times in my DOM.

<div ng-controller="theController">
    content does not matter
</div>

So the controller is istantiated many times. This is a problem because if i put a watcher in the controller

theController = function($scope) {
    $scope.$on('myVar', function() {
        // run one time for each time the template is repeated
    })
}

Any ideas about how to avoid this? Thanks in advance.

UPDATE

OK, i will try to be clearer.

Perhaps i've a form, that is built dynamically on the basis of the response of an asynchronous request.

<form ng-controller="formController">
    <div ng-repeat="f in fields">
        <ng-inclide src="f.fields"></ng-include>
    </div>
</form>

The controller is something like:

function formController($scope) {
    $scope.fields = [{ template:'', ... }];
    // data come from an ajax request... 
    // here are static for the sake of simplicity.
}

So I don't know what fields will be appended in the form.

The form field structure is stored in html partials... something like:

<div ng-controller="inputController">
    <label> .... </label>
    <input type="text" .... />
</div>

or

<div ng-controller="selectController">
    <label> .... </label>
    <select>
        ....
    </select>
</div>

function selectController($scope){
    $scope.$on("myCustomEvent", function(event) {
        cionsole.info("Options were updated");
    });
}

When the form has more than an input type=text, or a select, the inputController, or the selectController are instantiated more than once.

Why do you not want the $watch to occur for every instance?

I would like to update the options of one of the selects in the page when a specific event occurs.

What i get is instead that i update all the select in the page.

From the comment, i understood that is wrong to have more element with the same controller in the same page. So currently the only available solution seem to me that is to avoid to define a controller for each element of the form, right?

UPDATE 2

$emit is used in the inputController:

function inputController() {
    $scope.fireclick = function(p) {
        if (p == 'specificInput') {
            /* this is a temporary work around 
            I used to bind the event only with a specific field */

            $scope.$emit("myCustomEvent");      
        }
    }
}

This is the complete code of the input field used in the html partial:

<input type="text" ng-click="fireclick(f.name);" name="{{f.name}}" />

@Anybody:

Could at least confirm, (and eventually say why), to have on the same page more elements with the same controller is wrong

masoud ramezani
  • 22,228
  • 29
  • 98
  • 151
Bruno
  • 5,961
  • 6
  • 33
  • 52

3 Answers3

5

I think the Angular Way to do this is to use directives. I would do something like an ng-switch in your main view's ng-repeat, and have the ng-switch just include the appropriate directive... Assuming an "input-text" directive an an "input-dropdown" directive exist:

<div ng-swtich on="field.type" ng-repeat="field in fields">
    <div ng-switch-when="text"><input-text ...></div>
    <div ng-switch-when="dropdown"><input-dropdown ...></div>
</div>

I believe this way you will not have the same issue you're having now. I haven't actually setup what you're trying to do, but I'm 99% certain that you should be using directives! They are ideal for the type of thing you're doing, and will be much more re-usable.

I use a directive on http://angularlist.com to handle the ratings, and I can confidently say they don't cross wires when I have more than one on the page - That said, I'm not watching anything there, just responding to events... Actually, let me test something (testing.........) Yes! I added a watch to the model value the my ratings directive is editing and when clicking on a rating, only ONE watcher is fired - the one for the controller in question. This isn't on the live site, just my dev server here at home, but here's the directive if it helps you:

app.directive("angularStars", function() {
  return {
    restrict: 'E',
    scope: {
      model: '=ngModel',
      notifyId: '=notifyId'
    },
    replace: true,
    transclude: true,
    template: '<div><ol class="angular-stars">' + '<li ng-class="{active:model>0,over:over>0}">1</li>' + '<li ng-class="{active:model>1,over:over>1}">2</li>' + '<li ng-class="{active:model>2,over:over>2}">3</li>' + '<li ng-class="{active:model>3,over:over>3}">4</li>' + '<li ng-class="{active:model>4,over:over>4}">5</li>' + '</ol></div>',
    controller: function($scope, $attrs, $http) {
      $scope.over = 0;

      // TEST WATCH
      $scope.$watch('model', function() {
        console.log('modelChange', $scope.model);
      });

      $scope.setRating = function(rating) {
        $scope.model = rating;
        $scope.$apply();
        if ($attrs.notifyUrl !== void 0 && $scope.notifyId) {
          return $http.post($attrs.notifyUrl, {
            id: $scope.notifyId,
            rating: rating
          }).error(function(data) {
            if (typeof data === 'string') {
              alert(data);
            }
            return $scope.model = 0;
          });
        }
      };
      return $scope.setOver = function(n) {
        $scope.over = n;
        return $scope.$apply();
      };
    },
    link: function(scope, iElem, iAttrs) {
      if (iAttrs.notifyUrl !== void 0) {
        return angular.forEach(iElem.children(), function(ol) {
          return angular.forEach(ol.children, function(li) {
            li.addEventListener('mouseover', function() {
              return scope.setOver(parseInt(li.innerHTML));
            });
            li.addEventListener('mouseout', function() {
              return scope.setOver(0);
            });
            return li.addEventListener('click', function() {
              return scope.setRating(parseInt(li.innerHTML));
            });
          });
        });
      }
    }
  };
});

Directives are tough to get your head wrapped around - I am still mainly just shooting in the dark with them, trying things out to see how they work - but no doubt - the power you need is in the directives. I highly recommend reading the AngularJS docs on writting directives and then spending time looking at other people's directives - there are many available on GitHub to learn from!

Thom Porter
  • 2,604
  • 2
  • 19
  • 23
4

Here is how I managed to do a form with recursive fields (based on this SO answer: https://stackoverflow.com/a/15663410/1036025)

Result [image link]:

enter image description here

The view controller which loads the Home.html partial with a ng-view:

app.controller('HomeController', ['$scope', '$http', function ($scope, $http) {
    $scope.msg = 'Home Page Message';
}]);

The form controller which is inside Home.html:

app.controller('NestedFormCtrl', ['$scope', function ($scope) {
    $scope.formData = [
        {label:'First Name', type:'text', required:'true'},
        {label:'Last Name', type:'text', required:'true'},
        {label:'Coffee Preference', type:'dropdown', options: ["HiTest", "Dunkin", "Decaf"]},
        {label: 'Address', type:'group', Fields:[
            {label:'Street1', type:'text', required:'true'},
            {label:'City', type:'text', required:'true'},
            {label:'State', type:'dropdown',  options: ["California", "New York", "Florida"]}
        ]}
    ];

    $scope.$watch('formData[3].Fields[1].label', function(newval, oldval) {
        if (oldval !== newval) {
            console.log('watch', oldval, newval);
        }
    });

    // this was added after and is not shown in the image
    $scope.$watch('formData', function(newval, oldval) {
        if (oldval !== newval) {
            console.log('watch', oldval, newval);
        }
    }, true);

    $scope.changefield = function() {
        $scope.formData[3].Fields[1].label = 'Postal Code';
    }

    $scope.customevent = function(field) {
        var type = field.type;
        // do something for this type
        console.log('customevent', field);
    };
}]);

The Home partial view (here the template path in ng-include could be a property of your fields or you may use a switch case and display the input/select of your choice:

<h1>{{msg}}</h1>
<ul ng-controller="NestedFormCtrl">
    <li><button ng-click="changefield()">Change</button></li>
    <li ng-repeat="field in formData" ng-include="'views/field.html'"></li>
</ul>

The field.html template (either have one template per type of field, or one main template with a switch case on the field.type property)

<button ng-click="customevent(field)">{{field.label}}</button>
<ul>
    <li ng-repeat="field in field.Fields" ng-include="'views/field.html'"></li>
</ul>
Community
  • 1
  • 1
jpmorin
  • 6,008
  • 2
  • 28
  • 39
  • hello, thanks for your answer... could you share please also the code of views/field.html? – Bruno Apr 11 '13 at 21:08
  • It is the last block of code! It includes itself for recursion, but you first include it in your main view. – jpmorin Apr 11 '13 at 22:51
1

Here is a plunk to help you along for this scenario ...

http://plnkr.co/edit/RvFrmPd4rlAM8aeFSwg7?p=preview

This is just an illustration of how this can be achieved. Without knowing how much control you have over the form config data its not really possible to provide a more comprehensive answer. Between if you want to know, its better to inject $rootScope wherever you want to do a $broadcast ( $emit goes up the DOM tree while $broadcast goes down.. So $broadcasting from $rootScope will reach the whole app. )

Let me know if this helps you out.

ganaraj
  • 26,841
  • 6
  • 63
  • 59