I am using the ngStorage modules which can be found here: https://github.com/gsklee/ngStorage
I am setting a lastSeenId in localStorage for a live updating social feed, as this is a hybrid app (Laravel/AngularJS) I can't store it in $scope.
My StatusesController gets a array or data (or nothing if none to view) ever X seconds and I get the last item of the array and set my $localStorage var like this:
$localStorage.lastStatusSeen = lastId;
My navigation controller is where I set some new counters for the user to see if they have missed anything, I am getting lastSeenId like this:
$scope.$storage.lastStatusSeen which gets sent over to my API to return a number of unseen status posts.
This all works, user is on status page, if new data, then set the local storage var to the last ID in the array of data.
The issue is that my NavigationController is not finding this change and always passes the lastSeenId that was passed when the user first visited the page.
Is there a way to watch that key in local storage for changes?
EDIT: As requested, updated code
app.controllers.navigationController = ['$scope', 'pollingService', '$localStorage', function($scope, pollingService, $localStorage) {
$scope.lastStatusSeen = $localStorage.lastStatusSeen;
$scope.$watch(function () {
return $localStorage.lastStatusSeen;
}, function (newVal, oldVal) {
if (!newVal) {
return;
}
if (newVal !== oldVal) {
// Assign to a local scoped var, or anything, here...
$scope.lastStatusSeen = newVal;
// I suggest call a method to do your logic outside this
//$scope.onIdChange(newVal, oldVal); // If you need oldVal
}
});
pollingService.startPolling('emailCount', 'api/email/new-count', 5000, function(response) {
$scope.emailCount = response.data;
});
pollingService.startPolling('statusCount', 'api/social/new-count?lastid=' + $scope.lastStatusSeen, 5000, function(response) {
$scope.socialCount = response.data;
});
}];
You can watch it using the $watch method of any scope entity of angular ($scope or $rootScope):
$scope.$watch(function () {
return $localStorage.lastStatusSeen;
}, function (newVal, oldVal) {
if (!newVal) {
return;
}
if (newVal !== oldVal) {
// Assign to a local scoped var, or anything, here...
// I suggest call a method to do your logic outside this
$scope.onIdChange(newVal, oldVal); // If you need oldVal
}
});
Be careful with performance because this runs almost every $digest cycle. Another note is that you can associate this watcher to a variable, so you can cancel it anytime you need:
var watchIdStored = $scope.$watch(function () {
/* ... */
When you want to remove it, call the var as a function:
watchIdStored(); // Removes the $watcher
Edit 1:
The problem there is on pollingService.startPolling('statusCount', 'api/social/new-count?lastid=' + $scope.lastStatusSeen. It builds up a string that isn't changed anymore. You have to inform this change as it happens, providing to the poll the update.
If you put a console inside the watcher, where you update the var, you'll see that it's updating.
Edit 2:
As you have a stopPolling() method, you can create a build/destroy function on your controller.
function _setupPolling (name, url, pollingTime, callback) {
pollingService.stopPolling(name);
pollingService.startPolling.apply(pollingService, arguments);
}
Now, you call it on every change that occurs on $localStorage.lastStatusSeen:
$scope.$watch(function () {
return $localStorage.lastStatusSeen;
}, function (newVal, oldVal) {
if (!newVal) {
return;
}
if (newVal !== oldVal) {
console.log(newVal);
// Assign to a local scoped var, or anything, here...
$scope.lastStatusSeen = newVal;
// I suggest call a method to do your logic outside this
_setupPolling('statusCount', 'api/social/new-count?lastid=' + $scope.lastStatusSeen, 5000, function(response) {
$scope.socialCount = response.data;
});
}
});
Related
I've got a service for my websocket connection and this service returns in a function the actual state whether the client is actual connected or not.
I've simplified my code and these snippets are a small example of my problem:
myApp.factory('Websocket', function() {
//the status boolean whether connected or not
var bool = true;
//small mock for the status change
setInterval(function() {
bool = !bool;
}, 1000);
//i've made the value of the status variable accessable in a function or the variable itself
return {
status: function() {
return bool;
},
var: bool
}
});
Now i tried to apply the status value to my scope in the controller:
myApp.controller('Ctrl', function(Websocket, $scope) {
//method 1
$scope.status = Websocket.status();
//method 2
$scope.$watch(function() {
return Websocket.status();
}, function(newVal) {
$scope.status = newVal;
});
});
...but both methods don't work and don't update the $scope variable for this html:
<div ng-app="myApp">
<div ng-controller="Ctrl">
method1: {{status}}
</div>
</div>
This is a codepen: http://codepen.io/anon/pen/bVjaBa?editors=101
Thanks for your help!
The problem is as Nikos mentioned: you are changing the variable's value outside of the digest loop, so angular is not following the changes.
Using $scope.$apply is one option, but in this case I would prefer using $interval instead of setInterval.
myApp.factory('Websocket', function($interval) {
var bool = true;
$interval(function() {
bool = !bool;
}, 1000);
return {
status: function() {
return bool;
},
var: bool
}
});
See it working here.
The problem is that any "external" change to data watched by Angular has to call scope.$apply() for the watchers to update (here the watchers are the UI from the {{status}} expression). In the codepen the solution is simple: Make the service depend on the $rootScope and call $rootScope.$apply() in the interval function:
myApp.factory('Websocket', function($rootScope) {
...
setInterval(function() {
bool = !bool;
$rootScope.$apply();
}, 1000);
...
});
Something similar has to be done with the real service, key concepts being: (1) you have to call $apply() and (2) having no access to a specific scope, just use the $rootScope.
Hitting the ceiling of my Angular knowledge and I have been going around in circles on this.
Basically I have video player and chapter list directives, each with a controller. The controllers use the same model service which looks like this:
.service('VideoPlayerModel', function(){
var model = this;
model.chapters = {
chapterPos: 0,
targetChapter:null,
data: []
},
model.getVideoData = function() {
return model.videoData;
};
model.setVideoData = function(vData){
...
...
...
};
});
In the video player controller as the time of the player updates it finds the needed chapter data and updates the model.chapters data like this:
updateChapter: function(currentTime){
var chapters = VideoPlayerModel.chapters;
var chaptersCtrl = videoPlayerCtrl.chapters;
if (chapters.nextChapter.start <= currentTime) {
chapters.chapterPos = chapters.chapterPos + 1;
chaptersCtrl.setChapter(); //This finds and sets the Target Chapter
}
},
After setChapter runs I call console.log(VideoPlayerModel.chapters) and I can see the data model has updated with a result like this:
Object {chapterPos: 1, targetChapter: Object, data: Array[6], nextChapter: Object}
However the watch in the ChapterListCtrl doesn't fire and any of the onscreen items displaying the ChapterPos still show just the initial val of 0.
The controller looks like this:
.controller("ChapterListCtrl", ['$scope', 'VideoPlayerModel', function($scope, VideoPlayerModel) {
$scope.chapters = VideoPlayerModel.chapters;
$scope.$watch(function() { return VideoPlayerModel.chapters; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
}])
I have tried different ways and ended up with this, not sure if I am in the complete wrong direction now. Can anyone please help?
You don't need to use $watch, $broadcast or $on. This is best solved by regular JavaScript thinking.
Your problem is $scope.chapters = newValue; That is where you break the binding that your controllers use by introducing a new object unrelated to your service.
What you should to instead is to think about your service model.chapters = {..} and say hey! This is THE ONE object that I will use. And if I want to change the data in this object anywhere, I will switch the data inside the object and NOT assign a new object to the reference I use.
To do this I use the following methods:
transferDataList = function (from, to) {
/*
http://stackoverflow.com/questions/1232040/empty-an-array-in-javascript
*/
to.length = 0;
for (var i = 0; i < from.length; i++) { to.push(from[i]); }
};
transferDataMap = function (from, to) {
/*
http://stackoverflow.com/questions/684575/how-to-quickly-clear-a-javascript-object
*/
var member;
for (member in to) { delete to[member]; }
for (member in from) { to[member] = from[member]; }
};
And when I want to change the data in my object I DON'T do:
$scope.chapters = newValue;
Instead I do:
transferDataMap(newValue, $scope.chapters);
Or:
transferDataList(newValue, $scope.chapters);
This way you will keep your binding and your Angular interfaces will always be updated.
You can use $broadcast() and $on() function to achieve your requirement.
$broadcast() will flush an event to all it's child controller. So, you can $broadcast() an event with your new value to all controllers when you set a new value to your shared model.
Add a broadcast method in your shared service.
model.setVideoData = function(vData){
UpdateYourModel();
// Inform that your model is updated
$rootScope.$broadcast('modelUpdated');
}
And now you can add a listener for the event modelUpdated in all your controllers.
$scope.$on('modelUpdated', function () {
$scope.controllerModel = VideoPlayerModel.getVideoData(); // Update your controller model
}
And also, inject $rootScope to your service,
.service("VideoPlayerModel", ["$rootScope", function($rootScope){
// define your service here.
}] );
That's all !!!
I hope this will help you.
Try changing your watcher to:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
Alternatively if that doesn't achieve what you want, you can enable a deep watch by passing the third argument:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
}, true);
Your watcher doesn't fire because it always returns the same chapters which Angular considers as not-changed because it checks by reference. Your watcher can also be refactored as:
$scope.$watch(function() { return VideoPlayerModel.chapters.length; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
I want to call a function whenever a local variable has it's value changed.
I tried this code using a while loop but I get locked in the loop.
var titleChanged = false;
$scope.$watch('title',function(newValue, oldValue) {
if (newValue !== oldValue) {
titleChanged = true;
}
});
while (true) {
$timeout(checkForChangeAndSave, 3000);
};
function checkForChangeAndSave() {
if(titleChanged) {
updateNote();
titleChanged = false;
}
}
function updateNote() {
notesService.update($stateParams.id, {title: $scope.title});
notesService.getAll();
};
I recommend you use the interval service of angular.
The way you implemented it with while will block your code during the timeout execution.
Have a look at the the angular interval documention.
as I understand that you want to watch for a local variable not (attached to $scope) I may suggest this watch syntax
$scope.$watch(function(){return titleChanged;},function(newVal,oldVal){
// logic goes here
});
I am handling a JQRangeSlider through an angular directive + controller.
Within my controller, I am assigning new values to scope variables but there change is not notified to the controller scope (I am using isolated scope '=' in my directive) even though it is correctly updated.
// JQRangeSlider event handler
var valuesChanged = function(e, data) {
if (data.values.min && data.values.max) {
console.log('values changed', data.values); // this is always printed
$scope.minselectedtime = data.values.min; // not triggering $scope.$watch
$scope.maxselectedtime = data.values.max;
$scope.$broadcast('minchanged', $scope.minselectedtime); // instantly triggering $scope.$on('minchanged',...)
console.log('scope', $scope);
}
};
$scope.$watch('minselectedtime', function(newValue, oldValue) {
if (newValue === oldValue) {
return;
}
console.log('minselectedtime -->', newValue);
}, true);
$scope.$on('minchanged', function(event, min) {
console.log('minchanged ==>', min);
});
$scope.$watch('minselectedtime', ...) is only triggered on the next $scope modification (where $scope.minselectedtime is not even modified):
$scope.toggleTimeScale = function(refinementString) {
console.log('scale set to', refinementString);
$scope.timescale = refinementString; // year, month, day...
}
Why isn't $scope.$watch immediatly notified of the variable change?
The isolated code has its $scope.minselectedtime value changed but the parent scope doesn't see its value changed until $scope.toggleTimeScale is triggered.
Your method of using $scope.$apply() is correct. The reason being that the update from JQRangeSlider happened 'outside' of angularjs knowledge. If you read the documentation on the $apply method (http://docs.angularjs.org/api/ng.$rootScope.Scope#$apply) you'll see that it's used in the case you need.
I created a custom directive for the following widget attribute:
<div widget></div>
Basically the directive just creates some template code and puts the html inside the widget-tag.
Here's the directive:
skdApp.directive("widget", function() {
return {
restrict: 'A',
template: htmlTemplate,
link: function($scope, element, attri) {
/*
* setup event handler and twitter bootstrap components here
*/
}
}
The htmlTemplate is basic html code which also uses custom directives (e.g. the <seal> tag):
var htmlTemplate = '<div ng-controller="OfferCtrl">' +
'<div class="container">' +
<seal></seal>' +
'...'+
'</div>' +
'</div>';
In my Controller I first request some data to display in <div widget>.
I have an 'Offer' service that capsules all the logic to request data from the server. The method I'm using is called Offer.query().
skdApp.controller('OfferCtrl', function OfferCtrl ($scope, Offer) {
var o = Offer.query({id: 1}, function (response) {
$scope.offer = response;
});
});
In the response handler I'm binding the result to the scope.
The problem I'm facing now is that the directive also requests data but this request depends on the received data from Offer.query().
I.e. the response from Offer.query() returns an ID (let's call it myID) which is required by the seal directive to request more data.
Therefore I simply put all my logic in the callback Offer.query callback function. This doesn't seem to be the best way to do.
So I was thinking of moving this part to the link function of the <seal> directive:
skdApp.directive("seal", function() {
var sealHTML = '<div>{{offer.data.foobar}}</div>';
return {
restrict: 'E',
template: sealHTML,
link: function($scope, element, attrs) {
$scope.$watch('offer.myId', function (newValue, oldValue) {
if (typeof newValue !== "undefined") {
/* request more data with myId
* and bind the result to offer.data
*/
}
});
}
});
Is this approach 'angular'-compliant or is there some other better way (in terms of structure) to do this in angular?
You are able to watch offer.myId in child directive because both parent(widget) and child(seal) shares the same scope.By default directive does not create a new scope.
I think you broadcast custom event to notify the child directive and isolate scope if required to avoid scope clobbering.
http://docs.angularjs.org/api/ng.$rootScope.Scope
I'll float this Reverse-Jeopardy style (A question in the form of an answer). I've been mulling over a solution to this problem I saw recently. It clearly works, but it has some behavioral traits that I was first tempted to label as "wrong". On deeper reflection, I realized that those traits may be desirable in some very specific scenarios.
I would certainly not pitch this as a general solution to use each time you run into a need to bind data that is returned asynchronously. I present it to highlight the fact that the questions posed by this scenario have multiple potential answers. In some cases, there may be a genuine business logic need to block the UI rendering until the service call returns. In other cases, keeping the UI live and responsive for unrelated work may be more appropriate.
For example, in the case of an order processing system, I might very well want to block the client thread from interacting with view elements until the result of a sales transaction was known. The same could not be said of something like a web-based spreadsheet application with server-side formula calculations.
I think its a wonderful thing that both asynchronous and synchronous modes of addressing this need can co-exist. That is to say, returning a promise object does not obligate a client to use it any more than placing the return values in a scope obligates a client to watch them.
What follows demonstrates a way of handling this requirement synchronously alongside the previously explored asynchronous watch( ) style.:
var servicesModule = servicesModule || angular.module('myModule', []);
servicesModule.factory('thingClient',
['$http', '$q', function( $http, $q ) {
return new ThingClient($http, $q);
}]
);
function ThingClient($http, $q) {
function callService(scopeObject) {
var defer = $q.defer();
var promise = defer.promise;
$http({
method: 'POST',
url: 'http://at/some/url/',
data: { x: 'y', y: 'z', z: 'x', one: 1, two: 2, three: 3},
cache: false
}, function(data) {
scopeObject.data = data;
defer.resolve(data);
}, function() {
scopeObject.data = null;
defer.resolve(null)
});
return promise;
}
}
client-service.js
function ConsumingController( $scope, thingClient ) {
// Be careful to use an object (so you have data wrapped such that it
// is eligible for prototypical inheritance and call by reference without
// passing the scope itself). Leave the 'data' element undefined so you
// can trigger the transition from undefined -> null in the failure case
// and not just the on-success case.
$scope.dto = { data: undefined };
var promise = thingClient.callService(scope.dto);
// Asynchronous strategy
$scope.$watch('offer.myId', function (newValue, oldValue) {
if( newValue == null ) {
// Fail!
} else {
// Succeed!
}
}
// Synchronous strategy
if( promise.then(
function(data) {
if( data == null ) {
// Fail
} else {
// Succeed
}
}
}
}
consuming-controller.js