So, I have a global function thats called by a different library, that I'd like to impact my scope when its called.
In my controller I have:
angular.module('myApp')
.controller('myCtrl', function ($scope) {
$scope.nextAvailable = false;
$scope.thirdParty = window.thirdPartyFn = function() {
$scope.nextAvailable = true;
$scope.$apply();
};
});
When I log nextAvailable in the console after, it does return true (within that function obviously, so I'm confident its being called properly) - but it does not seem to update the surrounding scope within the enclosing controller. Any ideas?
Related
For an old angular app (version 1), I was asked to upload some data via it. I used Selenium to execute a javascript script that replaces the Angular app's $scope upload function to something I can work with.
ie
angular.element(document.querySelector('#somecontroller')).scope().uploadFunc() { ... }
Unfortunately, the new function does not have access to the $scope and various local non $scope functions found within that library.
ie.
...uploadFunc() {
localNonScopeFunc // ERROR: localNonScopeFunc not defined
$scope // ERROR: $scope not defined
}
I was able to get access to $scope indirectly but I still can't access any local functions.
I'm pretty sure I just need to bind the controller's this to function to resolve both issues but not sure how...
How would I bind the replaced $scope function to the angular app?
Update 1:
// existing library
var someApp= angular.module('wApp', ['oc.lazyLoad', 'lookup','menu','prompt','service']);
someApp.controller('somecontroller', function ($scope, $timeout, $interval, $http, $ocLazyLoad, $rootScope, service)
{
$scope.uploadFunc = function() {
$scope.doSomething();
NonScopeLibraryFunc();
...bad blocking code
};
}
function NonScopeLibraryFunc() {
...
}
I have to change the uploadFunc code since its blocking functionality. So I try
// selenium JavaScriptExecutor
angular.element(document.querySelector('#somecontroller')).scope().uploadFunc = function () {
$scope.doSomething(); // Error: $scope not defined
NonScopeLibraryFunc() // Error: NonScopeLibraryFunc not defined
...better non-blocking code
};
Neither $scope or NonScopeLibraryFunc() can be used. I was able to indirectly use $scope but calling NonScopeLibraryFunc is still a no go.
I also tried binding
const s = angular.element(document.querySelector('#somecontroller')).scope();
const newUploadFunc = function () {
$scope.doSomething(); // Error: $scope not defined
NonScopeLibraryFunc() // Error: NonScopeLibraryFunc not defined
...better non-blocking code
}.bind(s);
s.uploadFunc = newUploadFunc;
But it also does work.
Following example of overloading an angular scope function should give you the basics of what you need.
Where you might run into issues is with any arguments that might be passed into the scope function from the view
// Non angular code
document.querySelector('button').addEventListener('click', function() {
const someVar = 'Local var text';
// get angular scope
const angScope = angular.element(document.querySelector('#ang-app')).scope()
console.log('Remote access $scope.txt = ', angScope.txt);
// store reference to original scope function
const oldFunc = angScope.func
// overload original function
angScope.func = function(){
// modify scope variable with local value
angScope.txt = someVar;
// call original scope function
oldFunc();
// if modifying the original scope that needs to be changed in view use $.apply()
angScope.$apply()
}
angScope.func();
});
// Angular app
angular.module('myApp', [])
.controller('main', function($scope) {
$scope.txt = 'Scope text';
$scope.func = function(){
console.log('controller func() called')
$scope.log()
}
$scope.log = function(){
console.log('Scope txt:', $scope.txt);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<button>Trigger modified scope func</button>
<div id="ang-app" ng-app="myApp" ng-controller="main">
Angular display: {{txt}}
</div>
I am trying to bind data to $scope within a callback function and display this in an html element.
Below is my angular code:
gAutocomplete.controller('geocoder', ['gdata', function($scope, gdata){
var geocoder = L.mapbox.geocoder('mapbox.places');
geocoder.query('New York', assign_geocode2());
function assign_geocode2() {
function assign_geocode(err, data) {
console.log(data);
$scope.lat = data.latlng[0];
$scope.lng = data.latlng[1];
console.log($scope.lat)
}
return assign_geocode;
};
}])
Below is HTML:
</div>
<div class="spacer50"></div>
<div class="center-block" style="width:600px" ng-cloak data-ng- controller='geocoder'>
{{"Chosen lat/long are"}} {{$scope.lat}} {{$scope.lng}}
</div>
I can see the controller gets executed, callback function is called and values are written to console.log. However, they are not propogated to HTML element. What could be happening?
Update
I am not using $timeout as below and getting errors that $timeout is not a function. i know I am using an intermediate tmp variable, but when I use $timeout in the closure, I still have the same issue.
gAutocomplete.controller('geocoder', ['$scope', 'gdata', '$timeout', function($scope, $timeout, gdata) {
var tmp = {}
var geocoder = L.mapbox.geocoder('mapbox.places');
geocoder.query('New York', assign_geocode2(tmp));
function assign_geocode2(tmp) {
function assign_geocode(err, data) {
tmp.lat = data.latlng[0],
tmp.lng = data.latlng[1]
}
return assign_geocode;
}
$timeout(function() {
$scope.lat = tmp.lat,
$scope.lng = tmp.lng,
console.log($scope)},0);
}
])
You're changing scope values from a non-angular event handler. This means you need to notify angular that, "hey, I've updated things, take note pls". AFAIK the ideal way of taking care of this is running the callback inside a $timeout call.
function assign_geocode(err, data) {
$timeout(() => {
console.log(data);
$scope.lat = data.latlng[0];
$scope.lng = data.latlng[1];
console.log($scope.lat)
});
}
Running this inside $timeout will cause angular to run a digest cycle and update scope values. You don't need to do this from events initiated by Angular, because it already knows its in a digest cycle. For example, services like $http take care of this for you.
Scope is the glue between application controller and the view. During the template linking phase the directives set up $watch expressions on the scope. The $watch allows the directives to be notified of property changes, which allows the directive to render the updated value to the DOM.
...
{{"Chosen lat/long are"}} {{lat}} {{lng}}
...
Example :
http://plnkr.co/edit/5TJJkYf21LlwPyyKjgTv?p=preview
https://docs.angularjs.org/guide/scope
I have a directive that should call a function whenever a service variable is changed.
The following code is inside the directive:
$rootScope.$watch('movesService.selectedMove', function() {
console.log(movesService.selectedMove);
if (movesService.selectedMove === "Punch") {
vm.pushToFightLog('Select Target');
}
if (movesService.selectedMove === "Fury") {
//Action
}
if (movesService.selectedMove === "Fortify") {
//Action
}
if (movesService.selectedMove === "Parry") {
//Action
}
}, true);
Service:
angular
.module('outerZone')
.service('movesService', movesService);
function movesService() {
var vm = this;
vm.selectedMove = "Punch";
}
The thing is that when the $watch is called the first time it is able to read the variable and log it to the console, but even when the variable is changed after that, it does not fire the function.
I'm fairly confident that $rootScope is properly injected, but here is the code just to double check.
angular
.module('outerZone')
.directive('fightDisplay', fightDisplay);
fightDisplay.$inject = ["alliesService", "enemiesService", "fightQueueService", "$timeout", "movesService", "$rootScope"];
function fightDisplay(alliesService, enemiesService, fightQueueService, $timeout, movesService, $rootScope) {
With the plunker you have provided,I've made some changes in the watch function as below.
$scope.$watch('test', function() { //scope variable should be watched like this
$scope.watchCount++
});//removed true for strict checking (===) as $scope.test is initalised as String 'Hello'
if we want a Strict checking then change $scope.test =0 and
$scope.$watch('test', function() {
$scope.watchCount++
},true);
Here is the working plunker.To know more how $watch() works check this blog
I cannot get a binded service value to update when it is changed. I have tried numerous methods of doing so but none of them have worked, what am I doing wrong? From everything I have seen, this seems like it should work...
HTML:
<div class="drawer" ng-controller="DrawerController">
{{activeCountry}}
</div>
Controller:
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService', function($scope, mapService) {
$scope.$watch(function() { return mapService.activeCountry }, function(newValue, oldValue) {
$scope.activeCountry = mapService.activeCountry;
});
}]);
Service:
angular.module('worldboxApp').
service('mapService', function(dbService, mapboxService, userService) {
this.init = function() {
this.activeCountry = {};
}
this.countryClick = function(e) {
this.activeCountry = e.layer.feature;
};
this.init();
});
I put a break point to make sure the mapService.activeCountry variable is being changed, but all that ever shows in the html is {}.
If you work with objects and their properties on your scope, rather than directly with strings/numbers/booleans, you're more likely to maintain references to the correct scope.
I believe the guideline is that you generally want to have a '.' (dot) in your bindings (esp for ngModel) - that is, {{data.something}} is generally better than just {{something}}. If you update a property on an object, the reference to the parent object is maintained and the updated property can be seen by Angular.
This generally doesn't matter for props you're setting and modifying only in the controller, but for values returned from a service (and that may be shared by multiple consumers of the service), I find it helps to work with an object.
See (these focus on relevance to ngModel binding):
https://github.com/angular/angular.js/wiki/Understanding-Scopes
If you are not using a .(dot) in your AngularJS models you are doing it wrong?
angular.module('worldboxApp', []);
/* Controller */
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService',
function($scope, mapService) {
//map to an object (by ref) rather than just a string (by val), otherwise it's easy to lose reference
$scope.data = mapService.data;
$scope.setCountry = setCountry; //see below
function setCountry(country) {
// could have just set $scope.setCountry = mapService.setCountry;
// however we can wrap it here if we want to do something less generic
// like getting data out of an event object, before passing it on to
// the service.
mapService.setCountry(country);
}
}
]);
/* Service */
angular.module('worldboxApp')
.service('mapService', ['$log',
function($log) {
var self = this; //so that the functions can reference .data; 'this' within the functions would not reach the correct scope
self.data = {
activeCountry: null
}; //we use an object since it can be returned by reference, and changing activeCountry's value will not break the link between it here and the controller using it
_init();
function _init() {
self.data.activeCountry = '';
$log.log('Init was called!');
}
this.setCountry = function _setCountry(country) {
$log.log('setCountry was called: ' + country);
self.data.activeCountry = country;
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="worldboxApp">
<div ng-controller="DrawerController">
<button ng-click="setCountry('USA')">USA</button>
<br />
<button ng-click="setCountry('AUS')">AUS</button>
<br />Active Country: {{data.activeCountry}}
</div>
</div>
In some case $watch is not working with factory object. Than you may use events for updates.
app.factory('userService',['$rootScope',function($rootScope){
var user = {};
return {
getFirstname : function () {
return user.firstname;
},
setFirstname : function (firstname) {
user.firstname = firstname;
$rootScope.$broadcast("updates");
}
}
}]);
app.controller('MainCtrl',['userService','$scope','$rootScope', function(userService,$scope,$rootScope) {
userService.setFirstname("bharat");
$scope.name = userService.getFirstname();
$rootScope.$on("updates",function(){
$scope.name = userService.getFirstname();
});
}]);
app.controller('one',['userService','$scope', function(userService,$scope) {
$scope.updateName=function(){
userService.setFirstname($scope.firstname);
}
}]);
Here is the plunker
Note:- In Some case if broadcast event is not fired instantly you may use $timeout. I have added this in plunker and time depends on your needs. this will work for both factories and services.
JQuery to Directive
I want to call a method from the scope of this directive but can't seem to work it out (if possible).
$("my-directive").first().scope().loadData();
Directive Looks Something Like This
I would like to call the loadData function from the directive code below.
app.directive("myDirective", function () {
return {
restrict: "E",
templateUrl: "..."
scope: {},
controller: function ($scope, $element, $attrs) {
var self = this;
$scope.loadData = function () {
...
};
}
};
});
Scope is accessible inside the directive
You can get any child of the element of which directive is applied and get scope of it.
$('my-directive').first().children(":first").scope().loadData()
Strajk's answer is correct!
When Code is Added Dynamically setTimeout Needed
In the following example detail row has a nested directive (random-testees). I get the scope from that to dynamically compile the directive (dynamic and late-bound). The setTimeout is needed because it seems to take a bit before the
var detailRow = e.detailRow;
// Compile the directive for dyanmic content.
angular.element(document).injector().invoke(function ($compile) {
var scope = angular.element(detailRow).scope();
$compile(detailRow)(scope);
});
// Get some data from directive.
var testId = detailRow.find("random-testees").attr("testid");
// Wait, and call method on the directive.
setTimeout(function () {
var randomTesteesScope = $("random-testees").first().children(":first").scope();
randomTesteesScope.loadTestees(this);
}.bind(testId),200);
Not Very Confident About This
This seems a little brittle since I was getting mixed results with a 100 ms timeout sometimes and error when the randomTesteesScope returned undefined.