$scope.$watch isn't firing after load in angular.js - javascript

I'm using angular.js and am trying to use $watch to fire a function when my variable changes. It fires when the data is initially loaded, but not after. I'm not sure exactly what is going on here?
Code is pasted below:
function gradeChart($scope, $http) {
$http.get('studentData.json').success(function(data) {
$scope.students = data;
});
$scope.$watch('students',function(change){
console.log('this fires on load but not after');
});
}

It is not clear what code runs "after" and updates $scope.students.
However, here are the two most common problems related to updating $scope arrays:
If you reassign $scope.students to a new array, the $watch may still be looking at the previous array (reference). Try using angular.copy() in your "after" code:
angular.copy(data, $scope.students);
If you are changing one of the elements of the array, you'll need to use either $watchCollection (if it is available in the version of Angular you are using) or check for object equality instead of reference (note 3rd parameter):
$scope.$watch('students',function(change){...}, true);

Related

Creating 2-way binding for elements created after initial compiliation

I created a directive that dynamically creates a form based on a json from the server. I'm trying to add ng-model attribute to the various input elements so that I'll be able to use the input values after the user has typed them in and clicked submit. The ng-model attribute seems to be added but 2-way databinding doesn't work.
EDIT: I'm calling buildForm from within the link function as seen below:
function link(scope, elem, attr, ctrl) {
//asyc request to the server, data here is a json object from the server
getMovieDataStructure({
onSuccess: (data) => {
scope.mdb = data;
buildForm(scope.mdb, elem);
},
onFail: (res) => {
console.log("ERROR getting it");
}
});
}
Here is some of the code from in the directive:
//mdb is an array of objects describing the form requirments
function buildForm(mdb, formElement) {
for(var i=0; i < mdb.length; i++) {
if(mdb[i].type == 'string') {
if(mdb[i].maxLength && mdb[i].maxLength > 1024) {
//if maxLength > 1024 put a text area instead
formElement.append(createTextArea({
id: mdb[i].fieldName,
placeholder: mdb[i].fieldName
}));
} else {
//add input field to the form
formElement.append(createTextInput({
id: mdb[i].fieldName,
placeholder: mdb[i].fieldName
}));
}
} else if(){
//some more cases
}
formElement.append("<br>");
}
//...some more code...
}
//one of the functions to create an input element
function createTextInput(data) {
var elem = angular.element("<input>");
elem.attr("type", "text");
elem.attr("id", data.id);
elem.attr("ng-model", data.id);
elem.attr("placeholder", data.placeholder);
return elem;
}
For example, a result of an input element on the html page could look like this:
<input placeholder="movie_name" ng-model="movie_name" id="movie_name" type="text"> </input>
And if I'll put the same tag directly to in the html file the 2-way binding works great.
What am missing here? Is there a better way to do this and I'm just overcomplicating things?
Somewhere after you update the form you will need to call $compile, otherwise angular will not be aware of your changes. See:
https://docs.angularjs.org/api/ng/service/$compile
Something to try would be to call $rootScope.apply() after you call the buildform method maybe. What may be happening is that you are making all these changes to the DOM after the digest cycle completes and angular won't know about your changes until the next cycle happens.
So in your case it will be:
buildForm(scope.mdb, elem);
scope.$apply();
Thing is digest loop needs to be called explicitly in your case cause angular is unaware of the change made.
USE:
buildForm(scope.mdb, elem);
scope.$apply();
OR
But there is a better way for using $apply:
scope.$apply(buildForm(scope.mdb,elem));
The difference is that in the first version, we are updating the values outside the angular context so if that throws an error, Angular will never know.
As wdanda mentioned, since the directive adds DOM elements, it needs to be compiled afterwards to let angular be aware of the changes
Short answer is that the line buildForm(scope.mdb, elem); has been changed to $compile(buildForm(scope.mdb, elem).contents())(scope); and '$compile' was added to the directive's list of dependencies.
Long explanation:
buildForm(scope.mdb,elem) returns the element of the directive (so actually adding $compile(elem.contents())(scope); after buildForm would be equivilant), .contents() on an angular wraped element returns all of that element children.
That means that $compile(buildForm(scope.mdb, elem).contents()) tells angular to compile all the children of the directive's element, after buildForm has added some elements to it (and which some of them have directives of their own.
The call for .contents() is important because:
we only compile .childNodes so that we don't get into infinite loop compiling ourselves
(from https://docs.angularjs.org/api/ng/service/$compile)
The $compile() function returns a linking function that needs to be called with a scope to link to. So adding (scope) at the end will call that returned function.
A more clear (though slightly less elegant) way to write that code, would be:
var element = buildForm(scope.mdb, elem); //buildForm returns an angular wraped element
var linking = $compile(element); // $compile returns a linking function
linking(scope); //linking is functions that takes a scope object
//and needs to be run after compilation

Angular.js $scope.$on initial value

I'm using angular.js $scope.$on for listening for events from my service(event got triggered on service data change).
I use the following code snippet:
$scope.people = contacts.people;
$scope.$on('contacts-changed', () => {
$scope.people = contacts.people;
});
Is there any way to extend angular to avoid the first line and trigger $scope.$on('contacts-changed') or similar by default on the time of event bind?
I was looking in angular.js decorators but not sure how can I decorate $scope to add $scope.$bind or similar method
Ideas?
Sometimes the solution is so simple, but you need to think outside of the box. Move the event callback block into a function and just run it when the controller loads, after the event binding.
See this:
function handlePeople() {
$scope.people = contacts.people;
}
$scope.$on('contacts-changed', handlePeople);
handlePeople();
You can also keep the original code and do a trigger manually.
Temporally went with the following solution:
$rootScope.$bind = (bindName, bindHandler) => {
$rootScope.$on(bindName, bindHandler);
bindHandler();
};
But I'd want to replace it so I could use $scope instead of $rootScope and without defining this in each $scope...
I could try to use $scope._ proto _ but it seems _ proto _ is deprecated

Why is $watch not working like I expect - Angular

I am trying to watch a value in my controller. When it changes, I want to send out a broadcast, but I never get inside the $watch function. Here is the function:
$scope.$watch($scope.selectedEncounter, function(selectedEncounter) {
$scope.$broadcast('selecteRowChange', { encounter: selectedEncounter });
});
Can I watch something attached to the scope? If so what is the issue I am having with this code. If not, how do I implement this code to work?
You should pass either a function or a property name to your $watch function.
So, in your case, you should just change your code to:
$scope.$watch('selectedEncounter', function(value) {
// ...
});
Here is some more info from the docs.
The object you are watching is a complex object. Hence you should set objectEquality to true in your code as follows:
$scope.$watch('selectedEncounter', function(selectedEncounter) {
// ....
}, true);
Notice the true value as last parameter to the $scope.$watch function at the end.

executing Angular JS code after some jQuery.ajax has executed

I want to update an Angular scope with data returned by some jQuery ajax call. The reason why I want to call the Ajax from outside Angular is that a) I want the call to return as fast as possible, so it should start even before document.ready b) there are multiple calls that initialize a complex model for a multiple-page web app; the calls have dependencies among themselves, and I don't want to duplicate any logic in multiple Angular controllers.
This is some code from the controller. Note that the code is somewhat simplified to fit here.
$scope.character = {};
$scope.attributeArray = [];
$scope.skillArray = [];
The reasoning for this is that a character's attributes and skills come as objects, but I display them using ng-repeat, so I need them as arrays.
$scope.$watch('character',function(){
$scope.attributeArray = getAttributeArray($scope.character);
$scope.skillArray = getSkillArray($scope.character);
});
In theory, when $scope.character changes, this piece of code updates the two arrays.
Now comes the hard part. I've tried updating $scope.character in two ways:
characterRequestNotifier.done(function() { // this is a jQuery deferred object
$scope.$apply(function(){ // otherwise it's happening outside the Angular world
$scope.character = CharacterRepository[characterId]; // initialized in the jquery ajax call's return function
});
});
This sometimes causes $digest is already in progress error. The second version uses a service I've written:
repository.getCharacterById($routeParams.characterId, function(character){
$scope.character = character;
});
, where
.factory('repository', function(){
return {
getCharacterById : function(characterId, successFunction){
characterRequestNotifier.done(function(){
successFunction( CharacterRepository[characterId] );
});
}
};
});
This doesn't always trigger the $watch.
So finally, the question is: how can I accomplish this task (without random errors that I can't identify the source of)? Is there something fundamentally wrong with my approaches?
Edit:
Try this jsfiddle here:
http://jsfiddle.net/cKPMy/3/
This is a simplified version of my code. Interestingly, it NEVER triggers the $watch when the deferred is resolved.
It is possible to check whether or not it is safe to call $apply by checking $scope.$$phase. If it returns something truthy--e.g. '$apply' or '$digest'--wrapping your code in the $apply call will result in that error message.
Personally I would go with your second approach, but use the $q service--AngularJS's promise implementation.
.factory('repository', function ($q) {
return {
getCharacterById : function (characterId) {
var deferred = $q.defer();
characterRequestNotifier.done(function () {
deferred.resolve(CharacterRepository[characterId]);
});
return deferred.promise;
}
};
});
Since AngularJS has native support for this promise implementation it means you can change your code to:
$scope.character = repository.getCharacterById(characterId);
When the AJAX call is done, the promise is resolved and AngularJS will automatically take care of the bindings, trigger the $watch etc.
Edit after fiddle was added
Since the jQuery promise is used inside the service, Angular has no way of knowing when that promise is resolved. To fix it you need to wrap the resolve in an $apply call. Updated fiddle. This solves the fiddle, I hope it solves your real problem too.

How can I tell AngularJS to "refresh"

I have a click event that happens outside the scope of my custom directive, so instead of using the "ng-click" attribute, I am using a jQuery.click() listener and calling a function inside my scope like so:
$('html').click(function(e) {
scope.close();
);
close() is a simple function that looks like this:
scope.close = function() {
scope.isOpen = false;
}
In my view, I have an element with "ng-show" bound to isOpen like this:
<div ng-show="isOpen">My Div</div>
When debugging, I am finding that close() is being called, isOpen is being updated to false, but the AngularJS view is not updating. Is there a way I can manually tell Angular to update the view? Or is there a more "Angular" approach to solving this problem that I am not seeing?
The solution was to call...
$scope.$apply();
...in my jQuery event callback.
Why $apply should be called?
TL;DR:
$apply should be called whenever you want to apply changes made outside of Angular world.
Just to update #Dustin's answer, here is an explanation of what $apply exactly does and why it works.
$apply() is used to execute an expression in AngularJS from outside of
the AngularJS framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the AngularJS framework we need to perform proper scope life cycle of
exception handling, executing watches.
Angular allows any value to be used as a binding target. Then at the end of any JavaScript code turn, it checks to see if the value has changed.
That step that checks to see if any binding values have changed actually has a method, $scope.$digest()1. We almost never call it directly, as we use $scope.$apply() instead (which will call $scope.$digest).
Angular only monitors variables used in expressions and anything inside of a $watch living inside the scope. So if you are changing the model outside of the Angular context, you will need to call $scope.$apply() for those changes to be propagated, otherwise Angular will not know that they have been changed thus the binding will not be updated2.
Use
$route.reload();
remember to inject $route to your controller.
While the following did work for me:
$scope.$apply();
it required a lot more setup and the use of both .$on and .$broadcast to work or potentially $.watch.
However, the following required much less code and worked like a charm.
$timeout(function() {});
Adding a timeout right after the update to the scope variable allowed AngularJS to realize there was an update and apply it by itself.

Categories

Resources