Deep watch on array not working in AngularJS data binding - angularjs

I am using the popover directive from AngularStrap. I have a colleciton of objects that get rendered as toggles. When the toggles are clickedthe data on the scope is not changed... I've tried loads of different ways of doing this.
This is the controller of the popover
app.controller('CurvesPopoverCtrl', ['$scope', '$filter', 'AppData',
function($scope, $filter, AppData) {
'use strict';
$scope.popoverCurrencies = AppData.getCurrencies();
$scope.$watch ('popoverCurrencies', function(val) {
if(val !== null && val !== undefined){ // only fires when popover opens.
console.log("toggled", val);
//AppData.setCurrencies($scope.currencies);
}
}, true); // deep watch
}
]);
This is the template of the popover
<div class="well curves-popover" ng-controller="CurvesPopoverCtrl">
<div class="row">
<div ng-repeat="currency in popoverCurrencies">
<div class="clearfix" ng-if="$index % 3 == 0"></div>
<div class="col-md-4">
<div class="togglebutton">
<label>
<span ng-class="currency.iconClass"></span> <span>{{currency.ccy}}</span>
<input type="checkbox" checked="currency.build">
</label>
</div>
</div>
</div>
</div>
</div>

It gets a little complicated when you're attempting to watch a collection while using ng-repeat. According to the docs:
Each template instance gets its own scope ...
Therefore, the watch is not notified when the values within these other scopes are updated. One workaround is to bind a separate controller to each of the iterations of ng-repeat, as seen here. But another, possibly cleaner, approach is to utilize the ng-change directive to intercept the updating of these values. I explained it a bit here.

Related

AngularJS Scope not updating in view after async call

I am having trouble updating my scope on the front-end while making a request to an API. On the backend I can see that the value of my $scope variable is changing but this is not being reflected in the views.
Here is my controller.
Controllers.controller('searchCtrl',
function($scope, $http, $timeout) {
$scope.$watch('search', function() {
fetch();
});
$scope.search = "Sherlock Holmes";
function fetch(){
var query = "http://api.com/v2/search?q=" + $scope.search + "&key=[API KEY]&format=json";
$timeout(function(){
$http.get(query)
.then(function(response){
$scope.beers = response.data;
console.log($scope.beers);
});
});
}
});
Here is a snippet of my html
<div ng-if="!beers">
Loading results...
</div>
<p>Beers: {{beers}}</p>
<div ng-if="beers.status==='success'">
<div class='row'>
<div class='col-xs-8 .col-lg-8' ng-repeat="beer in beers.data track by $index" ng-if="beer.style">
<h2>{{beer.name}}</h2>
<p>{{beer.style.description}}</p>
<hr>
</div>
</div>
</div>
<div ng-if="beers.status==='failure'">
<p>No results found.</p>
</div>
I've tried several solutions including using $scope.$apply(); but this just creates the common error
Error: $digest already in progress
The following post suggested to use $timeout or $asyncDefault
AngularJS : Prevent error $digest already in progress when calling $scope.$apply()
The code I have above uses $timeout and I have no errors but still the view is not updating.
Help appreciated
I you are using AngularJS 1.3+, you can try $scope.$applyAsync() right after $scope.beers = response.data; statement.
This is what Angular documentation says about $applyAsync()
Schedule the invocation of $apply to occur at a later time. The actual time difference varies across browsers, but is typically around ~10 milliseconds. Source
Update
As others have pointed out, you should not (usually) need to trigger the digest cycle manually. Most of the times it just points to a bad design (or at least not an AngularJS-friendly design) of your application.
Currently in the OP the fetch method is triggered on $watch. If instead that method was to be triggered by ngChange, the digest cycle should be triggered automatically.
Here is an example what such a code might look like:
HTML
// please note the "controller as" syntax would be preferred, but that is out of the scope of this question/answer
<input ng-model="search" ng-change="fetchBeers()">
JavaScript
function SearchController($scope, $http) {
$scope.search = "Sherlock Holmes";
$scope.fetchBeers = function () {
const query = `http://api.com/v2/search?q=${$scope.search}&key=[API KEY]&format=json`;
$http.get(query).then(response => $scope.beers = response.data);
};
}
As the comments suggest, you shouldn't need to use $timeout to trigger a digest cycle. As long as the UX that elicits the change is within the confines of an angular construct (e.g. controller function, service, etc.) then it should manifest within the digest cycle.
Based on what I can infer from your post, you are probably using a search input to hit an API with results. I'd recommend changing the logic up such that you are triggering your search on an explicit event rather than the $watcher.
<input ng-model="search" ng-change="fetch()">
Remove the $watch logic and the $timeout wrapper.
function fetch(){
var query = "http://api.com/v2/search?q=" + $scope.search + "&key=[API KEY]&format=json";
$http.get(query)
.then(function(response){
$scope.beers = response.data;
console.log($scope.beers);
//it's a good habit to return your data in the promise APIs
return $scope.beers;
});
}
The reasons I make this recommendation is:
You have finer control of how the ng-change callback is triggered using ng-model-options. This means you can put a delay on it, you can trigger for various UX events, etc.
You've maintained a clearer sequence of how fetch is called.
You have possibly avoided performance and $digest issues.
Hey guys I solved the issue but I'm not sure exactly why this changed anything. Rearranging my code on JS Fiddle I just put all my partials into the index.html file like so and the requests and scope variables updated smoothly. Is was there perhaps a controller conflict with my html above?
<body ng-app="beerify" ng-controller='searchCtrl'>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container"><!-- nav bar code -->
</div>
</nav>
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1>Title</h1>
<form ng-submit="fetch()">
<div class="input-group">
<input type="text" ng-model="search"
class="form-control" placeholder="Search the name of a beer" name="srch-term" id="srch-term">
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button>
</div>
</div>
</form>
</div>
</div>
<div class="container">
<div ng-if="!beers">
Loading results...
</div>
<div ng-if="beers.status==='success'">
<div class='row'>
<div class='col-xs-8 .col-lg-8' ng-repeat="beer in beers.data track by $index" ng-if="beer.style">
<!-- ng-if will make sure there is some information being displayed
for each beer -->
<h2>{{beer.name}}</h2>
<h3>{{beer.style.name}}</h3>
<p>AbvMin: {{beer.abv}}</p>
<p>AbvMax: {{beer.ibu}}</p>
<p>{{beer.style.description}}</p>
<hr>
</div>
</div>
</div>
<div ng-if="beers.status==='failure'">
<p>No results found.</p>
</div>
</body>

Live search in AngularJS: updating the results

I want a live search: the results are queried from web api and updated as the user types.
The problem is that the list flickers and the "No results" text appears for a fraction of second, even if the list of results stays the same. I guess I need to remove and add items with special code to avoid this, calculating differences between arrays, etc.
Is there a simpler way to avoid this flicker at least, and probably to have possibility to animate the changes?
It looks like this now:
The html part is:
<div class="list-group">
<a ng-repeat="test in tests track by test.id | orderBy: '-id'" ng-href="#/test/{{test.id}}" class="list-group-item">
<h4 class="list-group-item-heading">{{test.name}}</h4>
{{test.description}}
</a>
</div>
<div ng-show="!tests.length" class="panel panel-danger">
<div class="panel-body">
No tests found.
</div>
<div class="panel-footer">Try a different search or clear the text to view new tests.</div>
</div>
And the controller:
testerControllers.controller('TestSearchListCtrl', ['$scope', 'TestSearch',
function($scope, TestSearch) {
$scope.tests = TestSearch.query();
$scope.$watch('search', function() {
$scope.tests = TestSearch.query({'q':$scope.search});
});
}]);
You should use ng-animate module to get the classes you need for smooth animation. For each ng-repeat item that's moved, added, or removed - angular will add specific classes. Then you can style those classes via CSS or JS so they don’t flicker.
An alternative way of doing what you require is to use the angular-ui bootstrap Typeahead component (check at the bottom of the post). It has a type-ahead-wait property in milliseconds and also a template url for customising it.
<div ng-app>
<div ng-controller="MyController">
<input type="search" ng-model="search" placeholder="Search...">
<button ng-click="fun()">search</button>
<ul>
<li ng-repeat="name in names">{{ name }}</li>
</ul>
<p>Tips: Try searching for <code>ann</code> or <code>lol</code>
</p>
</div>
</div>
function MyController($scope, $filter) {
$scope.names = [
'Lolita Dipietro',
'Annice Guernsey',
'Gerri Rall',
'Ginette Pinales',
'Lon Rondon',
'Jennine Marcos',
'Roxann Hooser',
'Brendon Loth',
'Ilda Bogdan',
'Jani Fan',
'Grace Soller',
'Everette Costantino',
'Andy Hume',
'Omar Davie',
'Jerrica Hillery',
'Charline Cogar',
'Melda Diorio',
'Rita Abbott',
'Setsuko Minger',
'Aretha Paige'];
$scope.fun = function () {
console.log($scope.search);
$scope.names = $filter('filter')($scope.names, $scope.search);
};
}

Directive doesn't work when I which the version of Angular to 1.0.1 to 1.2.27

The following could be run in demo here.
this is html:
<div ng-controller="MyCtrl">
<h2>Parent Scope</h2>
<input ng-model="foo"> <i>// Update to see how parent scope interacts with component scope</i>
<br><br>
<!-- attribute-foo binds to a DOM attribute which is always
a string. That is why we are wrapping it in curly braces so
that it can be interpolated.
-->
<my-component attribute-foo="{{foo}}" binding-foo="foo"
isolated-expression-foo="updateFoo(newFoo)" >
<h2>Attribute</h2>
<div>
<strong>get:</strong> {{isolatedAttributeFoo}}
</div>
<div>
<strong>set:</strong> <input ng-model="isolatedAttributeFoo">
<i>// This does not update the parent scope.</i>
</div>
<h2>Binding</h2>
<div>
<strong>get:</strong> {{isolatedBindingFoo}}
</div>
<div>
<strong>set:</strong> <input ng-model="isolatedBindingFoo">
<i>// This does update the parent scope.</i>
</div>
<h2>Expression</h2>
<div>
<input ng-model="isolatedFoo">
<button class="btn" ng-click="isolatedExpressionFoo({newFoo:isolatedFoo})">Submit</button>
<i>// And this calls a function on the parent scope.</i>
</div>
</my-component>
</div>
And this is js:
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})
.controller('MyCtrl', ['$scope', function ($scope) {
$scope.foo = 'Hello!';
$scope.updateFoo = function (newFoo) {
$scope.foo = newFoo;
}
}]);
This should be a good example for three kinds of scope binding in directives.However, it just doesn't work when I try to switch a higher angular version - (1.2.27). I suspect the shadow of the inherited scope within the directive, but I'm not sure of it.
This isn't going to work the way you expect. Isolated Scopes are created and provided to the Link, Compile, and Template portions of a Directive. However, the HTML within the Element itself is not actually part of the Directive. Those HTML portions are still bound to the parent $scope. If you have a tendancy to name your isolated scope objects the same, you may have just been working against the $scope unintentionally and not noticed any ill effect. If your HTML was in a Template rather than inside the Element, it would access the isolate scope.
As an example, in the HTML that is inline in the Element, you can call updateFoo(), but that would not be possible from inside a Template

Why does adding a show/hide feature break my AngularJS code?

Starting with the following working fiddle:
http://jsfiddle.net/77vXu/14/
I added a few changes to add a show/hide button
http://jsfiddle.net/77vXu/27/
var myApp = angular.module('myApp', []);
myApp.controller('test', function($scope) {
$scope.show = false;
$scope.cancelMessage = '';
$scope.clickTest = function(){
alert($scope.cancelMessage);
};
$scope.toggleShow = function(){
$scope.show = !$scope.show;
}
});
But this completely breaks the character counter. What have I done wrong?
From angularjs :Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored. The scope created within ngIf inherits from its parent scope using prototypal inheritance. An important implication of this is if ngModel is used within ngIf to bind to a javascript primitive defined in the parent scope. In this case any modifications made to the variable within the child scope will override (hide) the value in the parent scope.
Solution 1.
Please remove ng-if from textarea see here : http://jsfiddle.net/Tex3P/
<div ng-app="myApp">
<div ng-controller="test">
<button ng-if="!show" ng-click="toggleShow()">show me</button>
<div ng-if="show">
<textarea ng-model="cancelMessage" ></textarea>
<span > {{100 - cancelMessage.length}} characters remaining</span>
<button ng-click="clickTest()" ng-if="show">clickTest</button>
</div>
</div>
</div>
Solution 2.
Define cancelMessage as a object. http://jsfiddle.net/cnre6/
<div ng-app="myApp">
<div ng-controller="test">
<p>f{{cancelMessage}}</p>
<button ng-if="!show" ng-click="toggleShow()">show me</button>
<textarea ng-model="cancelMessage" ng-if="show"></textarea>
<span ng-if="show"> {{100 - cancelMessage.length}} characters remaining</span>
<button ng-click="clickTest()" ng-if="show">clickTest</button>
</div>
</div>
var myApp = angular.module('myApp', []);
myApp.controller('test', function ($scope) {
$scope.show = false;
$scope.cancelMessage = {};
$scope.clickTest = function () {
alert($scope.cancelMessage);
};
$scope.toggleShow = function () {
$scope.show = !$scope.show;
}
});
The reason it does not work is because of the way scope variables behave when they're assigned within a child scope and your model does not have a '.' in it. ng-if creates a child scope and since your ng-model does not have a '.' in it it will assign a scope variable named 'cancelMessage' in the child scope that shadows the scope variable in the 'test' controller's scope with the same name - effectively breaking two-way model binding as soon as text is entered in the textarea.
To fix this, you should have a '.' in your ng-model:
<textarea ng-model="cancelMessage.test" ng-if="show"></textarea>
By having a '.', angular will resolve what's left of the dot first, and will find the reference defined in the 'test' controller. It then binds the 'test' property of the 'cancelMessage' model.
The important point is, binding is resolving to the same model (the model which is defined on the 'test' controller's scope.
Infamous Dot in ng-Model (by Design)
Demo Plunker
If you refer to AngularJS documentation on ng-if, it says
"The ngIf directive removes or recreates a portion of the DOM tree based on an {expression}. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM." (https://docs.angularjs.org/api/ng/directive/ngIf)
One thing you can do is hide/show it instead of deleting it from DOM using ng-show or ng-hide
I demonstrate this in this fiddle : http://jsfiddle.net/lookman/0rfz6d1v/
<div ng-app="myApp">
<div ng-controller="test">
<button ng-if="!show" ng-click="toggleShow()">show me</button>
<div ng-show="show">
<textarea ng-model="cancelMessage" ></textarea>
<span > {{100 - cancelMessage.length}} characters remaining</span>
<button ng-click="clickTest()">clickTest</button>
</div>
</div>
</div>

Pass value between same controller with different divs

i don't no how do i explain my question,
ok let me try,
How to pass value from another div with same controller name?
<div ng-controller="myCtrl">
<select ng-model="filterBy" ng-options="filtration.title for filtration in filter_preferences"></select>
</div>
---------------------------------------------------
<div ng-controller="myCtrl">
<select ng-model="filterBy" ng-options="filtration.title for filtration in filter_preferences"></select>
<ul>
<li ng-repeat="vals in datas | filter: vals.completed = filterBy.value">{{vals.title}}</li>
</ul>
</div>
From the second div when i filter i works fine, but the same filter i've applied on the top with same controller but it doesn't work.
DEMO PLUNKER
The problem is that under each ng-controller it's creating a new $scope. This means they don't share $scope at all, and there are two different controller instances as well.
So, to communicate between two controllers, the common practice is to use a service.
I've updated your plunk with a very basic example.
The idea is that you create a service like:
app.value('sharedData', { filteredBy: true});
Then inject it into your controller and put it on your scope like so:
app.controller('MyCtrl', function($scope, sharedData) {
$scope.sharedData = sharedData;
});
Then after that you'd use it as your ng-model value and your filter:
<select ng-model="sharedData.filteredBy" ng-options="x.value as x.title for x in filter_preferences"></select>
<ul>
<li ng-repeat="vals in datas | filter: { completed: sharedData.filteredBy }">{{vals.title}}</li>
</ul>
From there it will work because both controllers (and $scopes) now have an instance of the same object... your sharedData service.
You need to specify
ng-controller="myCtrl"
where the ng-app is and remove all the other ng-controller statements. This is because whenever you are trying to do ng-controller in individual div it is creating a local scope for that particular div and then any change in the dropdown is refreshing that local scope.
Code:
<body ng-app="myApp" ng-controller="myCtrl">
<div >
<select ng-model="$parent.filterBy" ng-options="filtration.title for filtration in filter_preferences"></select>
</div>
---------------------------------------------------
<div>
<select ng-model="$parent.filterBy" ng-options="filtration.title for filtration in filter_preferences"></select>
<ul>
<li ng-repeat="vals in datas | filter: vals.completed = $parent.filterBy.value">{{vals.title}}</li>
</ul>
</div>
</body>
Controller Code change donw:
$rootScope.filterBy = $scope.filter_preferences[1];
Earlier it was:
$scope.filterBy = $scope.filter_preferences[1];
UPDATE:
You can have the filterBy variable on the rootScope rather than the local scope. So that any changes to the local scope will be done at the root level and changes will reflect everywhere
Tried this in your plunker and it is working.
You could not mention controller twice under ng-app

Resources