Bidirectional Binding not updating within ng-repeat - javascript

I'm having trouble with a bi-directional binding in an ng-repeat. I would expect for the below $watch to be triggered when you select a color from the list.
$scope.$watch('favoriteColors', function (newValue) {
console.log('example-favoriteColors', newValue);
});
I would expect for Orange to appear in $scope.favoriteColors when checked.
Example: http://plnkr.co/edit/k5SEQw4XFnxriD2I8ZG7?p=preview
directive('checkBox', function () {
return {
replace: true,
restrict: 'E',
//require: '^ngModel',
scope: {
'externalValue': '=ngModel',
'value': '&'
},
template: function (el, attrs) {
var html =
'<div class="ngCheckBox">'+
'<span ng-class="{checked: isChecked}">' +
'<input type="checkbox" ng-model="isChecked"/>'+
'</span>'+
'</div>';
return html;
},
controller: ['$scope', '$timeout', function ($scope, $timeout) {
var initialized = false;
console.log($scope.value());
if (angular.isArray($scope.externalValue)) {
$scope.isChecked = $scope.externalValue.indexOf($scope.value()) > 0;
} else {
$scope.isChecked = !!$scope.externalValue;
}
$scope.$watch('isChecked', function (newValue) {
if (angular.isDefined(newValue)) {
//add or remove items if this is an array
if (angular.isArray($scope.externalValue)) {
var index = $scope.externalValue.indexOf($scope.value());
if(index > -1) {
$scope.externalValue.splice(index, 1);
} else if (initialized) {
$scope.externalValue.push($scope.value());
}
} else {
//simple boolean value
$scope.externalValue = newValue;
}
if (initialized)
console.log($scope.externalValue);
}
});
$timeout(function () {
initialized = true;
});
}],
link: function (scope, el, attrs) {
}
};
});

Please check out this plunk: http://plnkr.co/edit/pbHz4ohBPi7iYq6uJI8X?p=preview
There were lots of changes. Some of them are:
The template needs not be a function, since it is static.
The initialized (and consequently the $timeout) is not needed.
I implemented my own indexOf function; there is a chance the objects are not the same in == sense, but equals in the x.name === y.name sense; (I have some doubts about this though)
The add or remove items if this is an array part was wrong; you need to update the array based on the value of isChecked, not based on whether the item already exists in the array (indexOf).
Initialize favoriteColors as an array, not as a single object, to be consistent, i.e. $scope.favoriteColors = [$scope.colors[1]];
(minor) Added a little more descriptive log when favoriteColors change.
Use $watch("favoriteColors", function() {...}, true) to watch for changes inside the array (not the true last argument).

I think it's because you need to be referencing a property on an object instead of the flat array. When you pass a primitive data structure like an array, it gets passed by reference and thus the updates aren't passed along properly. (Post by Mark Rajcok.)
I went ahead and showed this by hacking your plunkr a little bit. I changed $scope.favoriteColors = $scope.colors[1]; to $scope.favoriteColors = {value:$scope.colors[1]}; and changed <check-box ng-model="favoriteColors" value="color"> to <check-box ng-model="favoriteColors.value" value="color">.
Plunkr
You can see in the plunkr that when you hit the checkboxes the console.log statements now go off under the $watch function.

I see that you're using angular-form-ui's checkbox directive.
Use $watchCollection (link to documentation) instead of $watch for watching arrays for changes
Initialize $scope.favoriteColors as an array containing the values that should be checked
I've reverted your changes to angular-form-ui.js as those changes broke the directive. They are now exactly as the code appears in the latest commit on Github (checkbox.js). Only one thing has changed, the initialization of the angular-form-ui module by adding [] as the second argument to that first line.
Here is the updated plunker: http://plnkr.co/edit/mlUt46?p=preview

Related

Isolate scope two-way binding doesn't update in time for action to be invoked

I have an angular directive which acts as a control which lets the guest pick from multiple choices. When a choice is picked, a variable is assigned in the parent scope and a method is called to indicate that "the guest picked something."
The directive looks like this:
module.directive("myThingpicker", function () {
return {
restrict: "E",
scope: {
items: "=",
item: "=",
selected: "&"
},
templateUrl: "/templates/thing-picker.html",
controller: function ($scope) {
$scope.selectItem = function (item) {
$scope.item = item;
$scope.selected();
console.log ("The user picked an item:", item);
};
}
};
});
The template is straight forward: <li> elements generated using ng-repeat="x in items" with an ng-click="selectItem(x)" directive.
The problem is when selected is called, item hasn't passed its value up to the parent scope yet. Testing the bound value results in undefined. Clicking a second time (and causing a second selected() call shows the previous click's value.
In light of this fact, I'd either one of two possible solutions:
Pass the value as an argument to selected
If I pass an argument in to selected, I get an error Cannot use 'in' operator to search for...
Force the value of item to propagate before selected() call.
I would rewrite your $scope.selected(); to accept a passable argument item, that way when the function passed through the selected scope executes, it has the data it's looking for. The callback can then assign the item to a scope variable in the controller if that's what you wish for it to do.
Directive
module.directive("myThingpicker", function () {
return {
restrict: "E",
scope: {
items: "=",
selected: "&"
},
templateUrl: "/templates/thing-picker.html",
controller: function ($scope) {
$scope.selectItem = function (item) {
$scope.selected(item);
console.log ("The user picked an item:", item);
};
}
};
});
Controller
module.directive("fooController", function ($scope) {
$scope.model = []; //example of your items
$scope.selectCallback = function(item) {
// your code goes here
};
});
Markup
<my-thingpicker items="model" selected="selectCallback"></my-thingpicker>

What is the point of controller.$viewValue/controller.$modelValue?

I'm unclear what the relation is between scope.ngModel and controller.$viewValue/controller.$modelValue/controller.$setViewValue() is, and specifically, what the point of the latter three is. For example, see this jsfiddle:
<input type="text" ng-model="foo" my-directive>
and:
myApp.directive('myDirective', function($timeout) {
return {
require: 'ngModel',
restrict: 'A',
scope: { ngModel: '=' },
link: function (scope, element, attrs, controller) {
function log() {
console.log(scope.ngModel);
console.log(controller.$viewValue);
console.log(controller.$modelValue);
}
log();
controller.$setViewValue("boorb");
log();
scope.$watch('ngModel', function (val) {
console.log("val is now", val);
});
$timeout(function () {
log();
}, 2000);
}
}
});
With the controller being:
function MyCtrl($scope, $timeout) {
$scope.foo = 'ahha';
$timeout(function () {
$scope.foo = "good";
}, 1000);
}
The output is:
(index):45 ahha
(index):46 NaN
(index):47 NaN
(index):45 ahha
(index):46 boorb
(index):47 boorb
(index):53 val is now ahha
(index):53 val is now good
(index):45 good
(index):46 boorb
(index):47 boorb
controller.$viewValue did not start out as the value of the foo variable. Further, controller.$setViewValue("boorb") didn't influence scope.ngModel at all, nor was the update reflected in the HTML. Thus it seems there is no relation between scope.ngModel and controller.$viewValue. It seems that with anything I'd want to do, I would just use scope.ngModel, and watch those values. What is ever the point of using controller.$viewValue and controller.$modelValue or keeping them up to date with scope.ngModel?
scope: { ngModel: '=' }, creates an isolated scope for the directive, which means that changes to foo in the directive will no longer be reflected in the parent scope of MyCtrl.
Also, changes made by $setViewValue() will not get reflected in the DOM until controller.$render() is called, which tells Angular to update the DOM in the next digest cycle.
But to answer the question, NgModelController and its methods are really only necessary if you need to create some extra-special-custom-fancy data-binding directives. For normal data input and validation, you shouldn't ever need to use it. From the documentation (emphasis mine):
[NgModelController] contains services for data-binding, validation, CSS updates, and value formatting and parsing. It purposefully does not contain any logic which deals with DOM rendering or listening to DOM events. Such DOM related logic should be provided by other directives which make use of NgModelController for data-binding to control elements. Angular provides this DOM logic for most input elements.
The confusion here is coming from sticking a directive onto an existing directive, namely ngInput.
Instead, consider a fresh directive:
<my-directive ng-model="ugh">Sup</my-directive>
With:
$rootScope.ugh = 40;
And:
.directive('myDirective', function () {
return {
require: "ngModel",
// element-only directive
restrict: "E",
// template turns the directive into one input tag
// 'inner' is on the scope of the *directive*
template: "<input type='text' ng-model='inner'/>",
// the directive will have its own isolated scope
scope: { },
link: function (scope, element, attrs, ngModelCtrl) {
// formatter goes from modelValue (i.e. $rootScope.ugh) to
// view value (in this case, the string of twice the model
// value + '-'
ngModelCtrl.$formatters.push(function (modelValue) {
return ('' + (modelValue * 2)) + '-';
});
// render does what is necessary to display the view value
// in this case, sets the scope.inner so that the inner
// <input> can render it
ngModelCtrl.$render = function () {
scope.inner = ngModelCtrl.$viewValue;
};
// changes on the inner should trigger changes in the view value
scope.$watch('inner', function (newValue) {
ngModelCtrl.$setViewValue(newValue);
});
// when the view value changes, it gets parsed back into a model
// value via the parsers, which then sets the $modelValue, which
// then sets the underlying model ($rootScope.ugh)
ngModelCtrl.$parsers.push(function (viewValue) {
var sub = viewValue.substr(0, viewValue.length-1);
return parseInt(sub)/2;
});
}
};
})
Try it on Plunker.
Note that typeof ugh stays "number", even though the directive's view value is of a different type.

digest loop shows no change in my array (and view does not refresh)

I am retrieving an array of names from a service call. This service call takes in a filter parameter, which is initially empty when first executed (i.e. when I first show the table of names).
I have a filter input where the user can type characters to filter the list of names. This filter parameter gets passed to the service call and updates the list of names.
When stepping through the code, I see that my list of names has been updated and applies the new filter. I also step into the angularjs source and see that the apply \ digest loop automatically occurs right after the list of names has been updated. However, it does not refresh the view because inside the angular code, I see that the length of the array appears unchanged (even though up in my controller, the array is smaller due to the filter).
I don't understand why angular does not see the updated array?
View:
// My view contains the following input box where the user types in the string to filter with.
// The custom directive calls the action when focus is lost from the input (on blur).
<div class="input-group" data-ng-controller="myController">
<input my-custom-directive action="doFilter()" ng-model="name.filter">
</div>
// Display the names using another custom directive: nameList
<div data-ng-controller="myController" name-list></div>
Controller:
myModule
.controller('myController', ['$scope', 'Names', function ($scope, Names) {
$scope.name = { filter: '' };
$scope.doFilter = function () {
Names($scope.name.filter).query().$promise.then(function (data) {
$scope.names = data;
});
}
// Call doFilter to get the list of names when first enter the controller (filter is empty)
$scope.doFilter();
}
]);
Resource:
myModule
.factory('Names', ['$resource', '$rootScope', function ($resource, $rootScope) {
return function (name) {
return $resource(
'api/getnames/:Name', {
name: name
});
}
}])
Finally, the list of names is displayed using the custom nameList directive:
myModule
.directive("nameList", function () {
return {
link: function(scope, element, attrs) {
var update = function() {
var data = scope["names"];
}
var watcherFn = function(watchScope) {
return watchScope.$eval(data, data[i]);
}
scope.$watchCollection('names', function (newValue, oldValue) {
update();
});
},
template:
"<table class='table table-hover'>"
+ "<tbody>"
+ "<tr ng-repeat='n in names'>"
+ " <td>{{n.first}}</td>"
+ " <td>{{n.last}}</td>"
+ " </tr>"
+ "</tbody>"
+ "</table>"
}
})
When myController is first entered, I see a full list of names as expected, because doFilter is called when the controller is first entered, with an empty filter.
When I enter some characters in the filter input, I see that $scope.names has been upated inside doFilter with a smaller list of names.
However the view does not update. If I step into angular source code, particularly inside the $digest function:
if (watch) {
if ((value = watch.get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
: (typeof value === 'number' && typeof last === 'number'
&& isNaN(value) && isNaN(last)))) {
I see that the current value is always the same as the last value (when I hit my specific watch collection), and the current array is still the original large sized name array (whereas higher up in the call stack, the name array was updated to a smaller array due to the filter).
Why is angular not seeing my new sized array?
Note that I have also tried updating my $watchCollection to using just $watch (with true as the last parameter) with the same results, and I have also tried wrapping my $scopes.names = data in a $scope.$apply() again with no change.
You've got two myControllers there. Each will have their own scope and not a common one.
The usual solution is to use a service for the common data which can be injected into the controller.

Directive listen to controller variable change

Just starting out in AngularJS and trying to figure out the best practice for listening to events when a variable within a controller changes. The only way I have gotten it to work is with an emit, as follows.
For example:
var app = angular.module("sampleApp", [])
app.controller("AppCtrl", function($scope){
$scope.elements = [
{
name: "test"
}
]
$scope.addElement = function() {
$scope.elements.push({
name: "test" + $scope.elements.length
})
$scope.$emit('elementsChanged', $scope.elements);
}
})
app.directive('render', function() {
var renderFunc = function() {
console.log("model updated");
}
return {
restrict: 'E',
link: function(scope, element, attrs, ngModel) {
scope.$on('elementsChanged', function(event, args) {
renderFunc();
})
}
}
})
This seems a bit wonky, and I feel like I'm working against the point of angular. I've tried to have a $watch on a model, but that doesn't seem to be working. Any help on this would be very appreciated, thanks!
I'm going to assume you're using unstable Angular, because $watchCollection is only in the unstable branch.
$watchCollection(obj, listener)
Shallow watches the properties of an object and fires whenever any of the properties change (for arrays, this implies watching the array items; for object maps, this implies watching the properties). If a change is detected, the listener callback is fired.
The 'Angular' way of doing this would be to watch an attribute in your directive.
<render collection='elements'></render>
Your directive
app.directive('render', function() {
var renderFunc = function() {
console.log("model updated");
}
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.$watchCollection(attrs.collection, function(val) {
renderFunc();
});
}
}
})
If you're doing this on stable angular, you can pass true as the last argument to scope.$watch, which will watch for equality rather than reference.
$watch(watchExpression, listener, objectEquality)
objectEquality (optional) boolean
Compare object for equality rather than for reference.
What's happening here is the collection attribute on the DOM element specifies which property on our scope we should watch. The $watchCollection function callback will be executed anytime that value changes on the scope, so we can safely fire the renderFunc().
Events in Angular should really not be used that often. You were right in thinking there was a better way. Hopefully this helps.

$observe multiple attributes at the same time and fire callback only once

I wonder is it possible to execute some callback only once after evaluation all (or only some) attributes of directive (without isolated scope). Attributes are really great to pass configuration to the directive. The thing is that you can observe each attribute separately and fire callback several times.
In the example we have a directive without isolated scope which observs two attributes: name and surname. After any change action callback is fired:
html
<button ng-click="name='John';surname='Brown'">Change all params</button>
<div person name="{{name}}" surname="{{surname}}"></div>
js
angular.module('app', []).
directive('person', function() {
return {
restrict: 'A',
link: function($scope, $elem, $attrs) {
var action = function() {
$elem.append('name: ' + $attrs.name + '<br/> surname: ' + $attrs.surname+'<br/><br/>');
}
$attrs.$observe('name', action);
$attrs.$observe('surname', action);
}
}
});
Plunker here.
So the effect is that after changing name and surname during one click, action callback is fired twice:
name:
surname: Brown
name: John
surname: Brown
So the question is: can action be fired only once with both name and surname values changed?
You can use $watch to evaluate a custom function rather than a specific model.
i.e.
$scope.$watch(function () {
return [$attrs.name, $attrs.surname];
}, action, true);
That will be run on all $digest cycles, and if $watch detects the return array (or however you want to structure your function's return value) doesn't match the old value, the callback argument to $watch will fire. If you do use an object as the return value though, make sure to leave the true value in for the last argument to $watch so that $watch will do a deep compare.
Underscore (or lo-dash) has a once function. If you wrap your function inside once you can ensure your function will be called only once.
angular.module('app', []).
directive('person', function() {
return {
restrict: 'A',
link: function($scope, $elem, $attrs) {
var action = function() {
$elem.append('name: ' + $attrs.name + '<br/> surname: ' + $attrs.surname+'<br/><br/>');
}
var once = _.once(action);
$attrs.$observe('name', once);
$attrs.$observe('surname', once);
}
}
});
So, I've ended up with my own implementation of observeAll method, which can wait for several changes of attributes during one call stack. It works however I'm not sure about performance.
Solution of #cmw seems to be simpler but performance can suffer for large number of parameters and multiple $digest phase runs, when object equality is evaluated many many times. However I decided to accept his answer.
Below you can find my approach:
angular.module('utils.observeAll', []).
factory('observeAll', ['$rootScope', function($rootScope) {
return function($attrs, callback) {
var o = {},
callQueued = false,
args = arguments,
observe = function(attr) {
$attrs.$observe(attr, function(value) {
o[attr] = value;
if (!callQueued) {
callQueued = true;
$rootScope.$evalAsync(function() {
var argArr = [];
for(var i = 2, max = args.length; i < max; i++) {
var attr = args[i];
argArr.push(o[attr]);
}
callback.apply(null, argArr);
callQueued = false;
});
}
});
};
for(var i = 2, max = args.length; i < max; i++) {
var attr = args[i];
if ($attrs.$attr[attr])
observe(attr);
}
};
}]);
And you can use it in your directive:
angular.module('app', ['utils.observeAll']).
directive('person', ['observeAll', function(observeAll) {
return {
restrict: 'A',
link: function($scope, $elem, $attrs) {
var action = function() {
$elem.append('name: ' + $attrs.name + '<br/> surname: ' + $attrs.surname+'<br/><br/>');
}
observeAll($attrs, action, 'name', 'surname');
}
}
}]);
Plunker here
I resolved the exact same problem that I had using another approach, though I was looking for different ideas. While cmw's suggestions is working, I compared its performance against mine, and saw that the $watch method is called far too many times, so I decided to keep things the way I had implemented.
I added $observe calls for both variables I wanted to track and bound them to a debounce call. Since they both are modified with very little time difference, both $observe methods trigger the same function call, which gets executed after a short delay:
var debounceUpdate = _.debounce(function () {
setMinAndMaxValue(attrs['minFieldName'], attrs['maxFieldName']);
}, 100);
attrs.$observe('minFieldName', function () {
debounceUpdate();
});
attrs.$observe('maxFieldName', function () {
debounceUpdate();
});
There are several ways presented to solve this problem. I liked the debounce solution a lot. However, here is my solution to this problem. This combines all the attributes in one single attribute and creates a JSON representation of the attributes that you are interested in. Now, you just need to $observe one attribute and have good perf too!
Here is a fork of the original plunkr with the implementation:
linkhttp://plnkr.co/edit/un3iPL2dfmSn1QJ4zWjQ

Categories

Resources