Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use ngModel with plain ngController instead of directive?

In my application I would like to preserve the option of using plain controllers for certain sections of code - as opposed to creating directives for one-off things that will never be re-used.

In these cases I often want to publish some data from the controller to be used in the contained section. Now, I am aware that I could simply bind items in the controller's scope, however I'd like to specify the "model" location explicitly just to make the code more maintainable and easier to read. What I'd like to use is ng-model as it would be used on a custom directive, but just along side my plain controller:

<div ng-controller="AppController" ng-model='fooModel'>
  {{fooModel}}
</div>

However I can see no way to get a reference to the generated ngModelController without using a directive and the 'require' injection.

I am aware that I could make my own attribute fairly easily by injecting the $attr into my controller and do something like:

<div ng-controller="AppController" my-model='fooModel'>
  {{fooModel}}
</div>

In which case I just manually take or parse the myModel value and stick my model into the $scope under that name. However that feels wrong in this case - I really only need one "model" for a controller and I'd prefer not to have to add this boilerplate to every controller when ngModel exists. (It's the principle of the thing!)

My questions are:

1) Is there some way to use ngModel along with a plain controller to get the effect above?

2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)

UPDATE: As suggested in answers below $element.controller() can be used to fetch the controller. This works (http://plnkr.co/edit/bZzdLpacmAyKy239tNAO?p=preview) However it's a bit unsatisfying as it requires using $evalAsync.

like image 341
Pat Niemeyer Avatar asked Aug 17 '14 04:08

Pat Niemeyer


2 Answers

2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)

The answer depends slightly on where you want to access the controller from.

From outside the element with ng-model

It requires "name" attributes on both the element with the ng-model attribute, and a parent form (or ngForm). So say you have the form with name myForm and the element with ng-model attribute with name myInput, then you can access the ngModelController for myFoo from the parent scope as myForm.myInput. For example, for debugging purposes:

<p>myFoo: {{myForm.myInput.$modelValue}}<p>
<form name="myForm">
  <div ng-controller="InnerController" name="myInput" ng-model="model.foo"></div>
</form>

as can be seen at http://plnkr.co/edit/IVTtvIXlBWXGytOEHYbn?p=preview

From inside the element with ng-model

Similar to the answer from @pixelbits, using $evalAsync is needed due to the order of controller creation, but you can alternatively use angular.element.controller function to retrieve it:

app.controller('InnerController', function($scope, $element) {
  $scope.$evalAsync(function() {
    $scope.myModelController = $element.controller('ngModel');
  });
});

Used, inside the controller to view it, for debugging purposes, as:

<div ng-controller="InnerController" ng-model="model.foo">
  <p>myFoo: {{myModelController.$modelValue}}<p>
</div>

As can be seen at http://plnkr.co/edit/C7ykMHmd8Be1N1Gl1Auc?p=preview .


1) Is there some way to use ngModel along with a plain controller to get the effect above?

Once you have the ngModelController inside the directive, you can change its value just as you would were you using a custom directive accessing the ngModelController, using the $setViewValue function:

myModelController.$setViewValue('my-new-model-value');

You can do this, for example, in response to a user action that triggers an ngChange handler.

app.controller('InnerController', function($scope, $element) {
  $scope.$evalAsync(function() {
    $scope.myModelController = $element.controller('ngModel');
  });

  $scope.$watch('myModelController.$modelValue', function(externalModel) {
    $scope.localModel = externalModel;
  });

  $scope.changed = function() {
    $scope.myModelController.$setViewValue($scope.localModel);
  };
});

Note the extra watcher on $modelValue to get the initial value of the model, as well as to react to any later changes.

It can be used with a template like:

{{model.foo}}
<div ng-controller="InnerController" ng-model="model.foo">
  <p><input type="text" ng-model="localModel" ng-change="changed()"></p>
</div>

Note that this uses ngChange rather than a watcher on localModel. This is deliberate so that $setViewValue is only called when the user has interacted with the element, and not in response to changes to the model from the parent scope.

This can be seen at http://plnkr.co/edit/uknixs6RhXtrqK4ZWLuC?p=preview


Edit: If you would like to avoid $evalAsync, you can use a watcher instead.

$scope.$watch(function() {
  return $element.controller('ngModel');
}, function(ngModelController) {
  $scope.myModelController = ngModelController;
});

as seen at http://plnkr.co/edit/gJonpzLoVsgc8zB6tsZ1?p=preview


As a side-note, so far I seem to have avoided nesting plain controllers like this. I think if a certain part of the template's role is to control a variable by ngModel, it is a prime candidate for writing a small directive, often with an isolated scope to ensure there are no unexpected effects due to scope inheritance, that has a clear API, and uses require to access the ngModelController. Yes, it might not be reused, but it does help enforce a separation of responsibilities between parts of the code.

like image 67
Michal Charemza Avatar answered Dec 21 '22 11:12

Michal Charemza


When you declare directives on an element:

<div ng-controller="AppController" ng-model='fooModel'>
  {{fooModel}}
</div>

You can retrieve the controller instance for any directive by calling jQlite/jQuery $element.data(nameOfController), where nameOfController is the normalized name of the directive with a $ prefix, and a Controller suffix.

For example, to retrieve the controller instance for the ngModel directive you can do:

var ngModelController = $element.data('$ngModelController');

This works as long as the ngModel directive has already been registered.

Unfortunately, ngController executes with the same priority as ngModel, and for reasons that are implementation specific, ngModel is not registered by the time that the ngController function executes. For this reason, the following does not work:

app.controller('ctrl', function ($scope, $element) {
     var ngModelController = $element.data('$ngModelController');

     // this alerts undefined because ngModel has not been registered yet
     alert(ngModelController);
});

To fix this, you can wrap the code within $scope.$evalAsync, which guarantees that the directives have been registered before the callback function is executed:

app.controller('ctrl', function ($scope, $element) {
     $scope.$evalAsync(function() {
        var ngModelController = $element.data('$ngModelController');
        alert(ngModelController);
     });
});

Demo JSFiddle

like image 25
pixelbits Avatar answered Dec 21 '22 10:12

pixelbits