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
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've been having a problem with trying to keep my model separate from my controller because of lack of sync between model and view. I have looked around and found that most of the time an apply would solve the issue. However, apply does not work at all for me (either when called from the root scope or the relevant scope using chrome). In this link I have a demo of pretty much the problem I have on my program but instead of intervals my program has asynchronous requests or just complicated functions that seem to also be missed by angular. In the demo I have 4 variables that should be getting updated on the view. One that is being watched by the scope, another that is being updated through a callback, another that is just plain dependent on the model and one that is being updated by passing the scope itself to the service. Out of the 4 only the callback and passing the scope to the service are the ones that update the view, even when I run apply after each update (on top of the one that already runs after each execution of $interval). What I'm trying to avoid is using tons of callbacks or promises whenever my data changes due to transformations since I have many different transformations that are possible. Is there anyway to do this or are callbacks and promises the only option?
var test = angular.module("tpg",[]);
test.controller("myctrl", function($scope, $interval, service)
{
$scope.$watch(service.list.name, function()
{
$scope.name=service.list.name;
});
$scope.op=service.list.op;
$scope.call=service.list.call;
$scope.scope=service.list.test;
$scope.update=function()
{
service.getValues(function(op){$scope.op=op}, $scope);
};
}).factory("service", function($interval, $rootScope)
{
return {
list:{name:"OPA", op:"TAN", call:"1", test:"scope"},
getValues:function(callback, $scope)
{
var self=this;
var interval = $interval(function()
{
if(self.count>2)
{
$interval.cancel(interval);
self.count=0;
self.list={name:"OPA", op:"TAN", call:"1"};
}
else
{
self.list=self.values[self.count];
callback(self.list.op);
$scope.scope=self.list.test;
console.log(self.list);
self.count++;
}
$rootScope.$$phase || $rootScope.$apply();
},2000);
},
values: [{name:"guy", op:"ungly", call:"2", test:"scope1"}, {name:"TAL", op:"stink", call:"3", test:"scope2"}, {name:"tes", op:"test", call:"4", test:"scope3"}],
count:0
};
});
You need only a callback function to be returned from a service. $scope.$apply is not required when dealing with angular services as the service itself triggers the digest run. So I modified the code to remove the $apply and the promise and had a simple callback returned from the service which is then updating the view with the returned data.
Code:
$scope.update=function()
{
service.getValues(function(data){
$scope.name = data.name;
$scope.op=data.op;
$scope.call=data.call;
$scope.scope=data.test;
});
};
}).factory("service", function($interval, $rootScope)
{
return {
list:{name:"OPA", op:"TAN", call:"1", test:"scope"},
getValues:function(callback){
var self=this;
var interval = $interval(function()
{
if(self.count>2)
{
$interval.cancel(interval);
self.count=0;
self.list={name:"OPA", op:"TAN", call:"1"};
}
else
{
self.list=self.values[self.count];
console.log(self.list);
callback(self.list);
self.count++;
}
},2000);
},
values: [{name:"guy", op:"ungly", call:"2", test:"scope1"}, {name:"TAL", op:"stink", call:"3", test:"scope2"}, {name:"tes", op:"test", call:"4", test:"scope3"}],
count:0
};
});
Working plunkr
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.
I'm using BreezeJS with AngularJS but I'm having a difficult time understanding how to get Promises to work with $scope. Whenever I try to submit my form its not showing the validation errors until I click it for a 2nd time. I realize I could call $scope.$apply() but I read its not best practice? Here is my code:
app.controller("MainController", ["$scope", "$q", "datacontext", function ($scope, $q, datacontext) {
datacontext.manager.fetchMetadata();
$scope.errors = [];
$scope.addDamp = function () {
var item = datacontext.manager.createEntity("Damp", {
name: $scope.newDamp
});
var tes = datacontext.manager.saveChanges()
.then(function () {
alert("yay");
})
.fail(function (error, a, b, c) {
var arr = [];
error.entitiesWithErrors.map(function (entity) {
entity.entityAspect.getValidationErrors().map(function (validationError) {
arr.push(validationError.errorMessage);
});
});
$scope.errors = arr;
datacontext.manager.rejectChanges();
});
};
}]);
What is the best way to go about handling scope changes that come from inside of a Promise?
Yes, you're going to need $scope.apply here, because the promise isn't coming out of a core Angular call (such as $http, which would have handled the .apply() itself behind the scenes). In fact, the Breeze/Angular example on the BreezeJS page (http://www.breezejs.com/samples/todo-angular) includes a $scope.apply() after its data retrieval:
datacontext.getAllTodos()
.then(success)
.fail(failed)
.fin(refreshView);
function refreshView() {
$scope.$apply();
}
It's a bad practice to toss $scope.apply() about where you don't need it. But when you're handling promises created outside of Angular itself, it's going to come up.
I'm using AngularUI's uiMap directives to instantiate a google map. The uiMap directive works great with hard-coded data ({mapOptions} and [myMarkers]); however I run into trouble when I retrieve this data via $http.get() (the directive fires before the AJAX call has finished).
Initially I was executing the GET in my GoogleMaps controller, but when I realised things were happening out of sequence, I moved the GET into the uiMap directive. I've got 2 problems with this:
I think this is not the correct way to do this.
The GET also retrieves the data for [myMarkers]
The function/directive that creates the markers is ubiquitous in that it is responsible for creating all overlays
So my question is, is there somewhere else in the application where I can retrieve the data (and apply it to scope) before the directive runs?
I read up on $q, and that kind of sounds like what I want, but I'm not sure if I can do it within my controller rather than in the directive (also not sure how $q.defer.resolve() is any different than $http.success()).
EDIT Most of the code I'm using is copy/paste from AngularUI's doc, but here's a plunk: http://plnkr.co/edit/t2Nq57
Solution
Based on Andy's answer, I used a combination of uiMap and uiIf:
<!-- index.html -->
<div
id="map_container"
ng-controller="GoogleMaps">
<div ui-if="mapReady">
<div
ng-repeat="marker in markers"
ui-map-marker="markers[$index]"
ui-event="{'map-click':'openMarkerInfo(marker)'}"
></div>
<div
ui-map-info-window="myInfoWindow"
ng-include="'infobox.html'"
></div>
<div
id="map_canvas"
ui-map="myMap"
ui-options="mapOptions"
></div>
</div>
</div>
Caveat 1 uiIf cannot be in the same element that specifies the controller furnishing its condition (uiIf has higher priority than ngController, so its controller won't get set before uiIf executes).
Caveat 2 Be sure to use the most recent version of uiIf (the version supplied in the most recent tag, v0.3.2, is out of date). The old one has bug causing a TypeError under certain circumstances.
Caveat 3 jQuery MUST be included before AngularJS (in index.html); else you will receive a TypeError stating that Object [object Object] has no method 'trigger' (or Object [object HTMLDivElement] has no method 'trigger' on Windows). Chrome will allow you to step into the trigger function because Chrome knows about it, but Angular does not (and Angular is throwing the error).
function GoogleMaps( $scope , $http )
{
var mapDefaults = {
center: new google.maps.LatLng(25,-90),//centres on Gulf of Mexico
zoom: 4,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
$scope.mapOptions = {};
$scope.mapReady = false;
$scope.markers = [];
$http.get('map.json').then(function mapData(response) {
var map_data = response.data,
user_defaults = map_data.user.defaults; //{center: [lat,lng], zoom: 15}
$scope.mapOptions = {
"center": (typeof user_defaults.center !== 'undefined') ?
new google.maps.LatLng(user_defaults.center[0],user_defaults.center[1])
: mapDefaults.center,
"zoom": (typeof user_defaults.zoom !== 'undefined') ?
parseInt(user_defaults.zoom,10)
: mapDefaults.zoom,
"mapTypeId": mapDefaults.mapTypeId
};
//working on code to populate markers object
$scope.mapReady = true;
});
// straight from sample on http://angular-ui.github.com/#directives-map
$scope.addMarker = function($event) { … };
$scope.openMarkerInfo = function(marker) { … };
$scope.setMarkerPosition = function(marker, lat, lng) { … };
}//GoogleMaps{}
Drawback uiMap does not currently support rendering makers on domready. I'm looking into an alternative version of uiMapMarker suggested in this GitHub issue / comment.
Solution to this issue: https://stackoverflow.com/a/14617167/758177
Working example: http://plnkr.co/edit/0CMdW3?p=preview
You could just delay execution of ui-map until your data is loaded.
HTML:
<div ui-if="loadingIsDone">
<div ui-map="myMap" ui-options="myOpts"></div>
</div>
JS:
$http.get('/mapdata').then(function(response) {
$scope.myOpts = response.data;
$scope.loadingIsDone = true;
});
Generally, what you can do is have your directive get set up, start the load and finish in the success. I'm assuming you want to load one piece of data for all instances of your directive. So here's some psuedo-code for how you might want to attack this:
app.directive('myDelayedDirective', ['$http', '$q', function($http, $q) {
//store the data so you don't load it twice.
var directiveData,
//declare a variable for you promise.
dataPromise;
//set up a promise that will be used to load the data
function loadData(){
//if we already have a promise, just return that
//so it doesn't run twice.
if(dataPromise) {
return dataPromise;
}
var deferred = $q.defer();
dataPromise = deferred.promise;
if(directiveData) {
//if we already have data, return that.
deferred.resolve(directiveData);
}else{
$http.get('/Load/Some/Data'))
.success(function(data) {
directiveData = data;
deferred.resolve(directiveData);
})
.error(function() {
deferred.reject('Failed to load data');
});
}
return dataPromise;
}
return {
restrict: 'E',
template: '<div>' +
'<span ng-hide="data">Loading...</span>' +
'<div ng-show="data">{{data}}</div>' +
'</div>',
link: function(scope, elem, attr) {
//load the data, or check if it's loaded and apply it.
loadData().then(function(data) {
//success! set your scope values and
// do whatever dom/plugin stuff you need to do here.
// an $apply() may be necessary in some cases.
scope.data = data;
}, function() {
//failure! update something to show failure.
// again, $apply() may be necessary.
scope.data = 'ERROR: failed to load data.';
})
}
}
}]);
Anyhow, I hope that helps.
I am not sure if this will help without seeing code, but I ran into this same issue when I was creating my $scope.markers object inside the $http.success function. I ended up creating the $scope.markers = [] before the $http function, and inside the .success function, I populated the $scope.markers array with the return data.
So the $scope object was bound while the directive was compiling, and updated when the data returned.
[UPDATE SUGGESTION]
Have you tried taking advantage resolve in your route?
function($routeProvider) {
$routeProvider.
when(
'/',{
templateUrl: 'main.html',
controller: Main,
resolve: {
data: function(httpService){
return httpService.get()
}
}
}).
otherwise({redirectTo: '/'});
}
I usually put my $http requests in a service, but you could call the $http right from your route:
App.factory('httpService'), function($http){
return {
get: function(){
$http.get(url)
}
}
});
Then, in your controller, inject data and set your $scope items to the data.