How Angular "$watchs" arrays but wont watch properties? - angularjs

I have two controllers communicating via a service (factory).
OfficerCtrl and NotificationCtrl they communicate via the notification service.
Here's the NotificationCtrl and the notification service.
angular.module('transimat.notification', [])
.controller('NotificationsCtrl', function($scope, notification) {
$scope.msgs = notification.getMsgs();
$scope.showNotification = false;
$scope.$watchCollection('msgs', function() {
//put break point here: It enters
$scope.showNotification = $scope.msgs.length > 0;
});
$scope.close = function(index) {
notification.close(index);
};
$scope.showModal = false;
$scope.modalMsg = notification.getModalMsg();
$scope.$watch('modalMsg', function() {
//put break point here: Wont enter
$scope.showModal = $scope.modalMsg !== null;
},
true);
})
.factory('notification', function($interval) {
var severity = ['success','info','warning','danger','default'];
var msgs = [];
var modalMsg = null;
var notification = {
showSuccess: function(summary, detail, delay) {
showMsg(severity[0], summary, detail, delay);
},
//...
close: function(index) {
msgs.splice(index,1);
return msgs;
},
getMsgs: function() {
return msgs;
},
// MODALS
getModalMsg: function() {
return modalMsg;
},
showModalMsg: function(summary, detail, uri) {
modalMsg = {
summary: summary,
detail: detail,
uri: uri
};
},
closeModal: function() {
modalMsg = null;
}
};
var showMsg = function(severity, summary, detail, delay) {
var msg = {
severity: severity,
summary: summary,
detail: detail
};
msgs.push(msg);
if (delay > 0) {
$interval(function() {
msgs.splice(msgs.length - 1, 1);
}, delay, 1);
}
};
return notification;
});
That's my notification ctrl/service.
Now in my OfficerCtrl I "push" notifications via the notification service.
angular.module('transimat.officers', [])
.controller('RegisterOfficerCtrl', function (
$scope,
notification) {
// business logic
// this WONT work
$scope.showModal = function(){
notification.showModalMsg('NOW','BLAH','officer/1234');
}
// this works
$scope.showNotification = function() {
notification.showSuccess('Success', 'Blah blah.', 5000);
};
})
It will watch arrays but wont watch "normal" vars.
So I have to questions:
The showNotification() works, but the showModal() wont work. It has to do with some pointer thing? The Arrays have a "strong" pointer and the normal vars have "weak" pointers and get ignored/lost by the $scope.$watch expression?
How do I solve this?

The first watch watches the array of messages msgs of your controller scope, which is the array returned by the service getMsgs() function. So, each time the content of this array changes, the callback function is called. When showSuccess() is called, a new message is pushed to the array, and the callback is thus called.
The second watch watches the field modalMsg of your controller. So, each time a new value is assigned to $scope.modalMsg, or each time it's not equal to its previous value, the callback function is called. But a new value is never assigned to this variable. It's only assigned once, before the watch is created. The showModalMsg() function of the service assigns a new value to its own, private, modalMsg variable, but doesn't assign any new value to the controller's modalMsg variable, which still references the old notification modalMsg object:
Before showModalMsg():
$scope.modalMsg -----------------> object
^
|
notification modalMsg ---------------|
After showModalMsg():
$scope.modalMsg -----------------> object
notification modalMsg -----------> other object

Related

$scope.$watch does not seem to watch factory variable

I'm a beginner to angularjs. In my NFC project, I want to be able to GET from the server data based on a changing patientId.
However, I am not able to see my $watch execute correctly, even though I see that the patientId changes each time I scan a new NFC tag.
var nfc = angular.module('NfcCtrl', ['PatientRecordsService'])
nfc.controller('NfcCtrl', function($scope, NfcService, PatientRecordsService) {
$scope.tag = NfcService.tag;
$scope.patientId = NfcService.patientId
$scope.$watch(function() {
return NfcService.patientId;
}, function() {
console.log("Inside watch");
PatientRecordsService.getPatientRecords(NfcService.patientId)
.then(
function(response) {
$scope.patientRecords = response
},
function(httpError) {
throw httpError.status + " : " +
httpError.data;
});
}, true);
$scope.clear = function() {
NfcService.clearTag();
};
});
nfc.factory('NfcService', function($rootScope, $ionicPlatform, $filter) {
var tag = {};
var patientId = {};
$ionicPlatform.ready(function() {
nfc.addNdefListener(function(nfcEvent) {
console.log(JSON.stringify(nfcEvent.tag, null, 4));
$rootScope.$apply(function(){
angular.copy(nfcEvent.tag, tag);
patientId = $filter('decodePayload')(tag.ndefMessage[0]);
});
console.log("PatientId: ", patientId);
}, function() {
console.log("Listening for NDEF Tags.");
}, function(reason) {
alert("Error adding NFC Listener " + reason);
});
});
return {
tag: tag,
patientId: patientId,
clearTag: function () {
angular.copy({}, this.tag);
}
};
});
Not sure what I'm missing here - please enlighten me!
Update
Per rakslice's recommendation, I created an object to hold my data inside the factory, and now the html (with some server side delay) correctly displays the updated values when a new NFC tag is scanned.
var nfc = angular.module('NfcCtrl', ['PatientRecordsService'])
nfc.controller('NfcCtrl', function($scope, NfcService) {
$scope.tagData = NfcService.tagData;
$scope.clear = function() {
NfcService.clearTag();
};
});
nfc.factory('NfcService', function($rootScope, $ionicPlatform, $filter, PatientRecordsServi\
ce) {
var tagData = {
tag: null,
patientId: null,
patientRecords: []
};
$ionicPlatform.ready(function() {
nfc.addNdefListener(function(nfcEvent) {
//console.log(JSON.stringify(nfcEvent.tag, null, 4));
$rootScope.$apply(function() {
tagData.tag = nfcEvent.tag;
tagData.patientId = $filter('decodePayload')(tagData.tag.ndefMessage[0]);
PatientRecordsService.getPatientRecords(tagData.patientId)
.then(
function(response) {
tagData.patientRecords = response
},
function(httpError) {
throw httpError.status + " : " +
httpError.data;
});
});
console.log("Tag: ", tagData.tag);
console.log("PatientId: ", tagData.patientId);
}, function() {
console.log("Listening for NDEF Tags.");
}, function(reason) {
alert("Error adding NFC Listener " + reason);
})
});
return {
tagData: tagData,
clearTag: function() {
angular.copy({}, this.tagData);
}
};
});
Your code doesn't update the patientId value in the returned NfcService, only the local variable patientId inside the factory function.
Try saving a reference to the object you're returning in the factory function as in a local variable and use that to update the patientId.
For instance, change the creation of the object to put it in a local variable:
var nfcService = {
tag: tag,
patientId: patientId,
clearTag: function () {
angular.copy({}, this.tag);
}
};
...
return nfcService
and then change the patientId update to change the value in the object through the variable.
nfcService.patientId = $filter('decodePayload')(tag.ndefMessage[0]);
Update:
The basic fact about JavaScript that you need to understand is that when you assign one variable to another, if the first variable had a primitive data value the second variable gets a copy of that value, so changing the first variable doesn't affect the second variable after that, but if the first variable had an object reference the second variable gets pointed at that same object that the first variable is pointed at, and changing the object in the first variable after that will affect what you see through the second variable, since it's looking at the same object.
A quick experiment in the browser JavaScript console should give you the idea:
> var a = 1;
> a
1
> var b = a;
> b
1
> a = 5;
> a
5
> b
1
vs.
> var a = {foo: 1}
> var b = a
> a.foo = 5
> a.foo
5
> b.foo
5

Angular string binding not working with ControllerAs vm

I've been trying to do a two-way bind to a string variable on the Controller. When the controller changes the string, it isn't updated right away. I have already run the debugger on it and I know that the variable vm.overlay.file is changed. But it isn't updated on the View... it only updates the next time the user clicks the button that fires the selectOverlayFile() and then it presents the previous value of vm.overlay.file
Here goes the code:
(function () {
angular
.module("myapp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = [];
function SettingsController() {
var vm = this;
vm.overlay = {
file: undefined,
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.DATA_URL
}
};
vm.errorMessages = [];
vm.selectOverlayFile = selectOverlayFile;
vm.appMode = "photo";
vm.appModes = ["gif-HD", "gif-video", "photo"];
activate();
function activate() {
}
function selectOverlayFile() {
navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options);
}
function successOverlay(imageUrl) {
//If user has successfully selected a file
vm.overlay.file = "data:image/jpeg;base64," + imageUrl;
}
function errorOverlay(message) {
//If user couldn't select a file
vm.errorMessages.push(message);
}
}
})();
Thanks!
After a couple of hours searching for the issue and testing various solutions. I finally found it. The issue was that when the navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options) calls the callback function, it is out of AngularJS scope. So we need to notify Angular to update binding from within these callbacks using $scope.$apply():
(function () {
angular
.module("myapp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = ["$scope"];
function SettingsController($scope) {
var vm = this;
vm.overlay = {
file: undefined,
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.DATA_URL
}
};
vm.errorMessages = [];
vm.selectOverlayFile = selectOverlayFile;
vm.appMode = "photo";
vm.appModes = ["gif-HD", "gif-video", "photo"];
activate();
///////////////////
function activate() {
}
function selectOverlayFile() {
navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options);
}
function successOverlay(imageUrl) {
//If user has successfully selected a file
vm.overlay.file = "data:image/jpeg;base64," + imageUrl;
$scope.$apply();
}
function errorOverlay(message) {
//If user couldn't select a file
vm.errorMessages.push(message);
$scope.$apply();
}
}
})();

Angular $rootscope.$broadcast correct usage in a service

From what I've read, it seems using $rootScope.$broadcast is not advisable unless absolutely necessary. I'm using it in a service to notify a controller that a variable has changed. Is this incorrect? Is there a better way to do it? Should I be using watch instead (even though the variable only changes on user interaction) ?
the service:
function Buildservice($rootScope) {
var vm = this;
vm.box= [];
var service = {
addItem: addItem,
};
return service;
// Add item to the box
// Called from a directive controller
function addItem(item) {
vm.box.push(item);
broadcastUpdate();
}
function broadcastUpdate() {
$rootScope.$broadcast('updateMe');
}
// In the controller to be notified:
// Listener for box updates
$scope.$on('updateMe', function() {
// update variable binded to this controller
});
// and from a separate directive controller:
function directiveController($scope, buildservice) {
function addToBox(item){
buildservice.addItem(item);
}
So this works just fine for me, but I can't figure out if this is the way I should be doing it. Appreciate the help!
If you are in same module, why don't you use $scope instead of $rootScope?
You can use a callback function to notify the controller something has changed. You supply the service a function from the controller, and invoke that particular function whenever your variable has been changed. You could also notify multiple controllers if needed.
I have created a small example:
HMTL:
<div ng-controller="CtrlA as A">
{{A.label}}
<input type="text" ng-model="A.input" />
<button ng-click="A.set()">set</button>
</div>
<div ng-controller="CtrlB as B">
{{B.label}}
<input type="text" ng-model="B.input" />
<button ng-click="B.set()">set</button>
</div>
JS
var app = angular.module('plunker', []);
app.controller('CtrlA', function(AService) {
var vm = this;
vm.label = AService.get();
vm.notify = function() {
vm.label = AService.get();
}
vm.set = function() {
AService.set(vm.input)
}
AService.register(vm.notify);
});
app.controller('CtrlB', function(AService) {
var vm = this;
vm.label = AService.get();
vm.notify = function() {
vm.label = AService.get();
}
vm.set = function() {
AService.set(vm.input)
}
AService.register(vm.notify);
});
app.factory("AService", function() {
var myVar = "Observer";
var observers = [];
return {
get: function() {
return myVar;
},
set: function(name) {
console.log(name);
myVar = name;
this.notify();
},
register: function(fn) {
observers.push(fn);
},
notify: function() {
for( i = 0; i < observers.length; i++) {
observers[i]();
}
}
}
})
You will see upon executing this that the controllers get notified when the internal variable has been changed. (Notice: I haven't filtered the original sender from the list) (Plnkr)

UI not updating when property changed within $interval callback

I have a service which defines a background $interval. It watches local storage to see if some server updates didn't get made due to connectivity problems. When it finds some, it periodically tries to contact the server with the updates, deleting them from local storage when it succeeds.
Upon success, the callback also updates some properties of an angular view/model. Those changes should cause the UI to update, and elsewhere in the code of various controllers they do.
But within the $interval callback in that background service the changes do not cause the UI to update. I thought it might be some kind of failure to $apply, so I added a $rootScope.$apply() call. But that caused an error because a $digest was already in progress.
That tells me $digest is running after the callback executes -- which is what I would've expected -- but it's not causing the UI to update.
I then did a $rootScope.$emit() within the service, after the view/model was updated, and listened for the event within the controller where the UI should've updated. Within the listener I can see that the view/model the controller is based on was successfully updated by the service.
Here is the service:
app.factory('bkgndUpdates', function($interval, $q, $rootScope, dataContext, localStorage) {
var _interval = null;
var _service = {
get isRunning() { return _interval != null },
};
_service.start = function() {
if( _interval != null ) return;
_interval = $interval(function() {
// check to ensure local storage is actually available
// and has visits to process
var visits = localStorage.visits;
if( !visits || (visits.length == 0) ) return;
var recorded = [];
var promises = [];
var offline = false;
for( var idx = 0; idx < visits.length; idx++ )
{
var visit = visits[idx];
promises.push(dataContext.doVisitAction("/Walk/RecordVisit", visit)
.then(
function(resp) {
offline &= false;
var home = dataContext.findHomeByID(visit.addressID);
if( home != null ) {
// these values should cause a map icon to switch from yellow to blue
// elsewhere in the app that's what happens...but not here.
home.visitInfo.isDirty = false;
home.visitInfo.inDatabase = true;
home.visitInfo.canDelete = true;
home.visitInfo.followUpID = resp.FollowUpID;
}
recorded.push(visit.addressID);
},
function(resp) {
// something went wrong; do nothing, as the item is already
// in local storage
offline |= true;
})
);
}
$q.all(promises).then(
function(resp) {
for( var idx = 0; idx < recorded.length; idx++ )
{
localStorage.removeVisitByID(recorded[idx]);
}
if( !localStorage.hasVisits ) {
$interval.cancel(_interval);
_interval = null;
}
$rootScope.$emit("visitUpdate");
},
function(resp) {
alert("some promise failed");
});
}, 30000);
}
_service.stop = function() {
$interval.cancel(_interval);
_interval = null;
}
return _service;
});
Here's the controller for the map:
app.controller("mapCtrl", function ($scope, $rootScope, $location, dataContext) {
$scope.dataContext = dataContext;
$scope.mapInfo = {
center:dataContext.center,
zoom: dataContext.zoom,
pins: dataContext.pins,
};
$scope.$on('mapInitialized', function(evt, map){
$scope.dragend = function() {
$scope.dataContext.center = $scope.map.getCenter();
$scope.dataContext.getMapData($scope.map.getBounds());
}
$scope.zoomchanged = function() {
$scope.dataContext.zoom = $scope.map.getZoom();
$scope.dataContext.getMapData($scope.map.getBounds());
}
$scope.dataContext.getMapData($scope.map.getBounds())
.then(
function() {
$scope.mapInfo.pins = $scope.dataContext.pins;
},
function() {});
});
$scope.click = function() {
dataContext.pinIndex = this.pinindex;
if( dataContext.activePin.homes.length == 1)
{
dataContext.homeIndex = 0;
$location.path("/home");
}
else $location.path("/unit");
};
$rootScope.$on("visitUpdate", function(event) {
// I added this next line to force an update...even though the
// data on both sides of the assignment is the same (i.e., it was
// already changed
$scope.mapInfo.pins = $scope.dataContext.pins;
event.stopPropagation();
});
});
Here's the (partial) template (which relies on ngMap):
<div id="mapframe" class="google-maps">
<map name="theMap" center="{{mapInfo.center.toUrlValue()}}" zoom="{{mapInfo.zoom}}" on-dragend="dragend()" on-zoom_changed="zoomchanged()">
<marker ng-repeat="pin in mapInfo.pins" position="{{pin.latitude}}, {{pin.longitude}}" title="{{pin.streetAddress}}" pinindex="{{$index}}" on-click="click()"
icon="{{pin.icon}}"></marker>
</map>
</div>
So why isn't the UI updating?

Angular.js - Digest is not including $scope member changes

I have a service that includes:
newStatusEvent = function(account, eventId, url, deferred, iteration) {
var checkIteration;
checkIteration = function(data) {
if (iteration < CHECK_ITERATIONS && data.Automation.Status !== 'FAILED') {
iteration++;
$timeout((function() {
return newStatusEvent(account, eventId, url, deferred, iteration);
}), TIME_ITERATION);
} else {
deferred.reject('failure');
}
};
url.get().then(function(data) {
if (data.Automation.Status !== 'COMPLETED') {
checkIteration(data);
} else {
deferred.resolve('complete');
}
});
return deferred.promise;
};
runEventCheck = function(account, eventId, modalInstance, state) {
newStatusEvent(account, eventId, urlBuilder(account, eventId),
$q.defer(), 0)
.then(function() {
scopeMutateSuccess(modalInstance, state);
}, function() {
scopeMutateFailure(modalInstance);
})["finally"](function() {
modalEventConfig.disableButtonsForRun = false;
});
};
var modalEventConfig = {
disableButtonsForRun: false,
statusBar: false,
nodeStatus: 'Building',
statusType: 'warning'
}
function scopeMutateSuccess(modalInstance, state){
/////////////////////////////////////////////////
//THE SCPOPE DATA MEMBERS THAT ARE CHANGED BUT
//CURRENT DIGEST() DOES NOT INCLUDE THE CHANGE
modalEventConfig.statusType = 'success';
modalEventConfig.nodeStatus = 'Completed Successfully';
//////////////////////////////////////////////////
$timeout(function() {
scopeMutateResetValues();
return modalInstance.close();
}, TIME_CLOSE_MODAL);
state.forceReload();
}
modalEventConfig.scopeMutateStart = scopeMutateStart;
modalEventConfig.close = scopeMutateResetValues;
return {
runEventCheck: runEventCheck,
modalEventConfig: modalEventConfig
};
And here is the controller:
angular.module('main.loadbalancer').controller('EditNodeCtrl', function($scope, $modalInstance, Configuration, LoadBalancerService, NodeService, StatusTrackerService, $state, $q) {
NodeService.nodeId = $scope.id;
$q.all([NodeService.getNode(), LoadBalancerService.getLoadBalancer()]).then(function(_arg) {
var lb, node;
node = _arg[0], lb = _arg[1];
$scope.node = node;
return $scope.save = function() {
$scope.modalEventConfig.scopeMutateStart();
return NodeService.updateNode({
account_number: lb.customer,
ip: node.address,
port: node.port_number,
label: node.label,
admin_state: node.admin_state,
comment: node.comment,
health_strategy: {
http_request: "" + node.healthMethod + " " + node.healthUri,
http_response_accept: "200-299"
},
vendor_extensions: {}
}).then(function(eventId) {
return StatusTrackerService.runEventCheck(lb.customer, eventId,
$modalInstance, $state);
});
}
});
$scope.modalEventConfig = StatusTrackerService.modalEventConfig;
The issue I am having is in the service. After a successful resolve in newStatusEvent and scopeMutateSuccess(modalInstance, state); runs... the modalEventConfig.statusType = 'success'; and modalEventConfig.nodeStatus = 'Completed Successfully'; changes aren't reflected in the view.
Normally, this would be because a digest() is needed to make angular.js aware of a change. However, I have verified in the stack(chromium debugger) that a digest() was called earlier in the stack and is still in effect when the scope members are mutated in function scopeMutateSuccess(modalInstance, state);
What is weird, if I add $rootScope.$apply() after modalEventConfig.nodeStatus = 'Completed Successfully';...then Angular.js will complain a digest() is already in progress...BUT...the view will successfully update and reflect the new changes in from the scope members nodeStatus and statusType. But, obviously this is not the answer/appropriate fix.
So, the question is why isn't the digest() that is currently running from the beginning of the stack(stack from chromium debugger) making angular.js aware of the scope changes for modalEventConfig.statusType = 'success' and modalEventConfig.nodeStatus = 'Completed Successfully'? What can I do to fix this?
$scope.modalEventConfig = StatusTrackerService.modalEventConfig; is a synchronous call, you need treat things asynchronously .
You need wait on promise(resolved by service) at calling area also, i.e. in the controller .
Fixed it.
function scopeMutateSuccess(modalInstance, state){
/////////////////////////////////////////////////
//THE SCPOPE DATA MEMBERS THAT ARE CHANGED BUT
//CURRENT DIGEST() DOES NOT INCLUDE THE CHANGE
modalEventConfig.statusType = 'success';
modalEventConfig.nodeStatus = 'Completed Successfully';
//////////////////////////////////////////////////
$timeout(function() {
scopeMutateResetValues();
state.forceReload();
return modalInstance.close();
}, TIME_CLOSE_MODAL);
}
I am using ui-router and I do a refresh with it useing $delegate. I place state.forceReload(); in the $timeout...the scope members update as they should. I have no idea why exactly, but I am glad this painful experience has come to a end.

Resources