I have an application, call it a "form-filler" that works with many, many sites using Jquery to automatically update fields.
Pseudo Code:
Inject Jquery into the webpage
Discover the required form.
Update the values, e.g.,
$(document).ready(function) {
$('#id').val("some value");
}
I have a new customer who is using Angularjs and this model breaks down as the $scope is obviously being updated "out-of-band". I don't have access to the third party source to make changes, so I was wondering if it is possibly to get a jQuery update to trigger an Angularjs update?
You can use angular.element() to get a hold of the scope and the ngModelController:
var value = 'theNewValue';
var el = angular.element($('#name'));
el.scope().$apply(function(){
el.val(value);
el.controller('ngModel').$setViewValue(el.val());
});
Here is a simple example: http://plnkr.co/edit/OJQQmanwQoFQSgECuqal?p=preview
Agreeing on the other responses, I'd suggest to use $timeout instead of $apply to avoid problems with the digest phase.
Like in #liviu-t response, get hold of the $timeout service by means of the $element's injector. Then use it as it was a nextTick() function. It is in fact (with second argument 0 or missing) almost equivalent to nextTick(), with the difference that it always runs its argument in the digest phase, unlike $apply, which must be called outside of the digest.
It's a tad complicated depending on the actual case. My solution assumes that the elements are available on dom ready and not loaded by angular using partials. DEMO
JS
function setAngularValue($elem, value) {
var scope = $elem.scope();
var ngModelName = $elem.attr('ng-model');
if(ngModelName) {
var $injector = $elem.injector();
//get the parse service to use on the ng-model
var $parse = $injector.get('$parse');
var ngModel = $parse(ngModelName);
scope.$apply(function() {
//this will allow any ng-model value ex: my.object.value
ngModel.assign(scope, value);
});
}
else {
//fallback if there is no model, weird case imho
$elem.val(value);
}
}
$(document).ready(function() {
var $elem = angular.element('#myJqueryId');
var value = 'some value';
setAngularValue($elem, value);
});
HTML
<p>Hello {{my.angular.model}}!</p>
<input id="myJqueryId" ng-model="my.angular.model"></input>
LINKS
$injector
$parse
Related
I am writing an angular application as module inside an existing application.
There is a hidden field in the outer application that I need.
My question is what is the best way to fetch this value and use it in my angular application?
Thanks.
Add your attribute in window object from anywhere in your code, use
<script>
window.my_value = 'value';
</script>
And in your controller, please inject $window service and then use
app.controller("MyController", function ($scope, $window) {
console.log($window.my_value);
})
OR
You can directly use this hidden field's value, like
var obj = angular.element(document.querySelector("#hiddenFieldId"));
console.log(obj.val());
If you have an ID/Class for that input..
You can always get the value with pure JS:
var input = document.getElementById('id');
In the following code, is there only one $watch created even though the <input> element and interpolation of the double curlies {{customer.name}} create two different bindings to $scope.customer.name?
<html ng-app>
<body>
<input ng-model="customer.name" />
{{customer.name}}
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.12/angular.min.js"></script>
</body>
</html>
As a follow-up, does the resulting callback to the listener defined in the $watch update the DOM and re-render those changed elements?
Answer to question 1:
There will be two watchers added.
Slightly simplified, a watcher is an object that is added to the scopes $$watchers array. This watcher object has a watchFunction and a listenerFunction.
Even if you used $watch to register the same pair of watch- and listenerFunction twice, there would be two watcher objects added.
Answer to question 2:
It depends on the watcher.
Consider the interpolation in your example: {{ customer.name }}
The listenerFunction (the function that is executed when the watched expression has changed) of the associated watch object will look like this:
function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
}
Pretty straightforward. The listenerFunction will update the node's nodeValue with the new value. When Angular's digest loop is finished the execution leaves Angular and the JavaScript context, which is followed by the browser re-rendering the DOM to reflect changes. Again a bit simplified.
When it comes to the ng-model directive, it gets more complex, since it has a two-way data binding ($scope --> view and view --> $scope) while the previous case has a one-way data binding ($scope --> view).
The watcher added from ng-model doesn't even register a listenerFunction. Instead it performs work in the watchFunction.
I will not go deeper into the matter, but might as well put the code below (which can be found here):
$scope.$watch(function ngModelWatch() {
var value = ngModelGet($scope);
// if scope model value and ngModel value are out of sync
if (ctrl.$modelValue !== value) {
var formatters = ctrl.$formatters,
idx = formatters.length;
ctrl.$modelValue = value;
while (idx--) {
value = formatters[idx](value);
}
if (ctrl.$viewValue !== value) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value;
ctrl.$render();
}
}
return value;
});
Every ng-* and angular expression create internally a separate watcher.
If you want to check how many watchers are created you could check for angular private scope property scope.$$watchers
Sometimes there could be a lot of this watchers for a complex app, with some performance issue, that's why library as Bindonce was born (in the demo you could find interesting code to check watchers)
I'm currently playing with AngularJS. I'd like to return, from a service, a variable that will let the scope know when it has changed.
To illustrate this, have a look at the example from www.angularjs.org, "Wire up a backend". Roughly, we can see the following:
var projects = $firebase(new Firebase("http://projects.firebase.io"));
$scope.projects = projects;
After this, all updates made to the projects object (through updates, be it locally or remotely) will be automatically reflected on the view that the scope is bound to.
How can I achieve the same in my project? In my case, I want to return a "self-updating" variable from a service.
var inbox = inboxService.inboxForUser("fred");
$scope.inbox = inbox;
What mechanisms let the $scope know that it should update?
EDIT:
In response to the suggestions, I tried a basic example. My controller:
$scope.auto = {
value: 0
};
setInterval(function () {
$scope.auto.value += 1;
console.log($scope.auto.value);
}, 1000);
And, somewhere in my view:
<span>{{auto.value}}</span>
Still, it only displays 0. What am I doing wrong ?
UPDATE:
I made a demo plunker: http://plnkr.co/edit/dmu5ucEztpfFwsletrYW?p=preview
I use $timeout to fake updates.
The trick is to use plain javascript references:
You need to pass an object to the scope.
You mustn't override that object, just update or extend it.
If you do override it, you lose the "binding".
If you use $http it will trigger a digest for you.
So, whenever a change occurs, the scope variable reference to same object that gets updated in the service, and all the watchers will be notified with a digest.
AFAIK, That's how $firebase & Restangular work.
If you do multiple updates you need to have a way of resetting properties.
Since you hold a reference to an object across the application, you need to be aware of memory leaks.
For example:
Service:
app.factory('inboxService', function($http){
return {
inboxForUser: function(user){
var inbox = {};
$http.get('/api/user/' + user).then(function(response){
angular.extend(inbox, response.data);
})
return inbox;
}
};
});
Controller:
app.controller('ctrl', function(inboxService){
$scope.inbox = inboxService.inboxForUser("fred");
});
It depends on how the object is updating. If it gets updated "within" angular, a digest cycle will be triggered (See http://docs.angularjs.org/guide/scope), and the view will update automatically. That is the beauty of Angular.
If the object gets updated "outside" of angular (e.g. a jQuery plugin), then you can manually trigger a digest cycle by wrapping the code that's doing the updating in an $apply function. Something like this:
$scope.$apply(function() {
//my non angular code
});
See http://docs.angularjs.org/api/ng.$rootScope.Scope for more info.
I'm trying to get the service from my legacy code and running into a weird error with injector() returning undefined:
Check this plnkr
Also, I'm trying to set back the new property value back to the service, will that be reflected to the scope without the use of watch?
Thank you very much, any pointer or suggestion is much appreciated.
You're trying to get the element before the DOM has been constructed. It's basically the same issue as running javascript outside of a $(document ).ready(). So this line has no element to get:
var elem = angular.element($('#myCtr'));
Also, by the way, instead of using jQuery, another Angular option for doing the above is:
var elem = angular.element(document.querySelector('#myCtr'))
Angular provides an equivalent to $(document ).ready() called angular.element(document).ready() which we can use.
But you'll also need to grab scope and execute your change within a scope.$apply() so that Angular is aware that you've changed something that it should be aware of.
Combining the two we get:
angular.element(document).ready(function () {
var elem = angular.element($('#myCtr'));
//get the injector.
var injector = elem.injector();
scope= elem.scope();
scope.$apply(function() {
injector.get('cartFactory').cart.quantity = 1;
});
});
updated plnkr
I am using AngularJS and angularFire to show a list of my tasks:
<ul ng-repeat="tool in tools">
<li>{{tool.name}} {{ tool.description}}</li>
</ul>
var toolRef = new Firebase(dbRef + "/tools/" + toolId);
toolRef.once('value', function(snapshot) {
console.log(angular.toJson(snapshot.val()));
$scope.tools.push(snapshot.val());
//$scope.$apply();
});
http://jsfiddle.net/oburakevych/5n9mj/11/
Code is very simple: I bind a 'site' object to my Firebase DB. The object contains a list of ID of relevant tools. Then I load every tool in the $scope.tools variable and use ng-repeat to show them in the UI.
However, every time I push a new entry into my $scope.tools - the DOM is not updated. I have to call $scope.$apply to trigger digest after every push - see commented out line 18 and then it works.
It's really strange since I sow this several times now and only with scope variables bound with angularFire.
Can anyone explain this? Am I doing anything wrong?
I am not sure about the Firebase once method as how it work, but it seems like it is making changes to model outside of Angular context in it's callback and hence you need $scope.$apply.
In anuglar services like $http ,$resource, and $timeout internally call $scope.$apply so that you don't need to call it on the callback. If the Firebase system provides a method replacement for once which internally does apply or returns a promise, you can very well skip call to apply method again and again.
Because you change the scope outside of AngularJS, you must use $scope.$apply() to inform Angular about the scope changes. That is a common way to handle that. To use the error handling of AngularJS i would wrap the code in a callback function like:
$scope.$apply(function(){
$scope.tools.push(snapshot.val());
});
As tschiela says, you need to wrap the function. I use $timeout. So...
<ul ng-repeat="tool in tools">
<li>{{tool.name}} {{ tool.description}}</li>
</ul>
var toolRef = new Firebase(dbRef + "/tools/" + toolId);
toolRef.once('value', function(snapshot) {
$timeout(function() {
$scope.tools.push(snapshot.val());
})
});
To update scopes when you change them, you need to use the factory angularFireCollection of angularFire. Otherwise you are just calling a javascript class wich won't update scopes until $apply().
var getTool = function(dbRef,toolId) {
console.log("Gettingtool ID: " + toolId);
angularFireCollection(dbRef + "/tools/" + toolId, function(snapshot) {
console.log(angular.toJson(snapshot.val()));
$scope.tools.push(snapshot.val());
});
};
DEMO