I think this might be quite common use-case with any angular app. I am simply watching some objects on my scope that are changed as part of several digest cycles. After digesting them (changing their values via databinding) has finished, I want to save them to databse.
A. Now, with the current solutions I see following problems:
running save in $timeout() - how to assure that save is called only
once
running a custom function in $scope.$evalAsync - how to find out what has been chaged
There are of course solutions to both of these prolblems, but non of those I know seem ehough elegant to me.
The question is: What is the most elegant solution to the problem?
B. In particular, what are the best practices to
make sure that save gets called only once in a digest cycle
find out that object is dirty after last digest
Here is a solution I've found working best for me - as an AMD modul. Inspired by Underscore.
/**
* Service function that helps to avoid multiple calls
* of a function (typically save()) during angular digest process.
* $apply will be called after original function returns;
*/
define(['app'], function (app) {
app.factory('debounce', ['$timeout', function ($timeout) {
return function(fn){ // debounce fn
var nthCall = 0;
return function(){ // intercepting fn
var that = this;
var argz = arguments;
nthCall++;
var later = (function(version){
return function(){
if (version === nthCall){
return fn.apply(that, argz);
}
};
})(nthCall);
return $timeout(later,0, true);
};
};
}]);
});
/*************************/
//Use it like this:
$scope.$watch('order', function(newOrder){
$scope.orderRules.apply(newOrder); // changing properties on order
}, true);
$scope.$watch('order.valid', function(newOrder){
$scope.save(newOrder); //will be called multiple times while digested by angular
});
$scope.save = debounce(function(order){
// POST your order here ...$http....
// debounce() will make sure save() will be called only once
});
Related
I have been through all related topics on SO, namely these two:
$watch not detecting changes in service variable
$watch not detecting changes in service variable
are tackling the same issue, but i failed to make it working. Unlike in the above cases, I am using a controller from a component, hence maybe this is related to lacking binding in components, idk. Hope for some experinced assistance.
Have a service:
(function (angular) {
'use strict';
angular
.module('Test')
.service('ShareData', ShareData);
ShareData.$inject = [];
function ShareData() {
let vm = this;
vm.indexes = [];
vm.setIndexes = function(firstIndexParam, lastIndexParam, message) {
if (leaderIndexParam !== undefined || partnerIndexParam !== undefined) {
vm.indexes.mainIndex = firstIndexParam;
vm.indexes.secondaryIndex = lastIndexParam;
vm.indexes.message = message;
}
};
vm.getIndexes = function() {
return vm.indexes;
};
}
})(angular);
It is used in 3 components. Two of them are sending data into the service, the third one uses this data. Sending of data is accomplished in the following way, works:
ShareData.setIndexes(firstIndex, secondIndex, 'update_indexes');
Now here is my problem. In main parent controller i can comfortably access the data by
ShareData.getIndexes();
But my issue is that I need changes in indexes to trigger certain actions in parent controller, so I tried so do as stipulated by relevant questions here on SO:
$scope.$watch('ShareData.getIndexes()', function(newVal) {
console.log('New indexes arrived', newVal);
});
In main controller, I am injecting the service:
TabController.$inject = ['ShareData'];
and using it like:
let indexService = ShareData.getIndexes();
As i said, I can get the data when I am explicitly calling the function. The issue is that it needs to be triggered by the service itself.
It does not work regardless of shamanistic ceremonies a made :) Now, obviously, I am missing something. Should I somehow bind this service to the component, and if yes how is it done? Or maybe the solution is totally dysfunctional and impossible to achieve in my circumstances? An advise is appreciated!
UPDATE: I already have a functional solution with the same service working with $rootScope.$broadcast, however my aim is to get rid of it and not work with the $rootScope.
The problem is that you never actually change the value of vm.indexes - it always points to the same array. setIndexes only modifies properties of this array. That's why $watch, which by default checks for reference equality only, fails to spot the changes.
There are (at least) two ways of solving this: either make $watch check the object equality instead, by adding a third param there:
$scope.$watch('ShareData.getIndexes()', function(newVal) {
console.log('New indexes arrived', newVal);
}, true);
... or (better, in my opinion) rewrite the set function so that it'll create a new instance of indexes instead when there's a change:
vm.setIndexes = function(firstIndexParam, lastIndexParam, message) {
if (leaderIndexParam === undefined && partnerIndexParam === undefined) {
return;
}
vm.indexes = vm.indexes.slice();
Object.assign(vm.indexes, {
mainIndex: firstIndexParam,
secondaryIndex: lastIndexParam,
message: message
});
};
As a sidenote, simply calling setIndexes() does not trigger the digest - and $watch listener only checks its expression when digest is triggered.
So, what I basically want to have is visualisation of the socket connection for the user. Meaning an indication wheter the client is connected to the server over socket.io or not.
Therefor I have service which keeps track of the socket state:
[...]
.factory('DataConnection', function(CONN_EVENTS, socket, $rootScope) {
var dataConnection = {};
dataConnection.states={
// indicates if the user can interact with the ui
uiEnabled: false,
[...]
};
// socket IO connection states
$rootScope.$on(CONN_EVENTS.socketAuth, function() {
dataConnection.states.uiEnabled = true;
});
$rootScope.$on(CONN_EVENTS.socketUnAuth, function() {
dataConnection.states.uiEnabled = false;
});
$rootScope.$on(CONN_EVENTS.socketConnLost, function() {
dataConnection.states.uiEnabled = false;
});
}
which sets the states based on events. This events are emitted in a different service/factory which handles the socket events.
I tried many approaches to sync these values with my controller. It kinda worked with a manual $scope.$apply(), but sometimes it gave me the apply already in progress error and since its very bad practice, I decided to not use it.
I ended up with this solution in my controller:
.controller("MainCtrl", function($scope, DataConnection) {
$scope.$watch(function() {
return DataConnection.states.uiEnabled
}, function(uiEnabled) {
$scope.uiEnabled = uiEnabled;
}, true);
Which doesn't want to work somehow. When the socket get disconnected (the server gets shut down), the ui won't update. After interaction (e.g. trigger a popover), the ui will update as expected.
Is there anything I'm missing? Or something else I can try?
OK this is not a solution to get $watch working but instead just a workaround that "might" get your first approach using events to work without throwing a digest already in progress error. So instead of using $scope.$apply() enclose changes you make inside $timeout(function()) instead like this
$timeout(function(){
// do whatever changes you want here
}) // no timeout value
NOTE that there is no timeout actually added above. Why i think this might work is because a $timeout automatically applies whatever changes you made once script inside completes execution (ie, its like a $scope.$spply() that won't throw a digest cycle already in progress error). I am not sure about this but just give it a try.
I'd simply suggest you to use $scope.$applyAsync(), which means that if an apply in progress, it will do it on the next $digest only.
To me is it OK your approach, to watch a function that returns an object to be observed, I don't know what could be the problem, but I would use callbacks:
.factory('DataConnection', function(CONN_EVENTS, socket, $rootScope) {
var dataConnection = {};
dataConnection.states={
_uiEnabledBackingField: false,
// indicates if the user can interact with the ui
uiEnabled: function(val) {
if (typeof val !== 'undefined') {
this._uiEnabledBackingField = val;
this.callback(val);
return;
}
return this._uiEnabledBackingField;
},
onUiEnabled: function(cb){
if (cb) {
this.callback = cb;
}
}
};
});
And then in my controller:
.controller("MainCtrl", function($scope, DataConnection) {
dataConnection.states.onUiEnabled(function(uiEnabled){
$scope.uiEnabled = uiEnabled;
});
});
I did some research and this is my conclusion:
The $apply will trigger in three different cases (Exploring Angular 1.3: Go fast with $applyAsync) In my case (where I have to update the DOM after a socket event), the $apply has to be triggered manually.
This example is also found in the Angular Wiki:
If you're creating an AngularJS service (such as for sockets) it should have a $scope.$apply() anywhere it fires a callback.
I tried to trigger the $apply in the socket service already (instead of the controller), which worked way better and didn't give me the "$apply already in progress error".
I have the following example code in my learning app. The service does his job and pulls some data out of a page with json code generated by php, so far so good.
service:
(function() {
'use strict';
angular
.module('app.data')
.service('DashboardService', DashboardService);
DashboardService.$inject = ['$http'];
function DashboardService($http) {
this.getFormules = getFormules;
////////////////
function getFormules(onReady, onError) {
var formJson = 'server/php/get-formules.php',
formURL = formJson + '?v=' + (new Date().getTime()); // Disables cash
onError = onError || function() { alert('Failure loading menu'); };
$http
.get(formURL)
.then(onReady, onError);
}
}
})();
Then i call the getFormules function in my controller and put all the data inside my $scope.formuleItems and test if everything succeeded and 'o no'... $scope.formuleItems = undefined! - Strange because my view is showing data?
part of the controller:
dataLoader.getFormules(function (items){
$scope.formuleItems = items.data;
});
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
The first thing i did was search around on stackoverflow to look if someone else had the same issue, and there was: Undefined variable inside controller function.
I know there are some walkarounds for this, i've done my own research, but something tells me that this (see example below) isn't the best way to solve this problem.
solution one: put $watch inside of the controller
$scope.$watch('formuleItems', function(checkValue) {
if (checkValue !== undefined) {
//put the code in here!
}
}
or even:
if($scope.formuleItems != null) {}
The rest of the controller is relying on $scope.formuleItems. Do i really have to put everything into that $watch or if? Can i fix this with a promise? I never did that before so some help would be appreciated.
The code in your callback
function (items){
$scope.formuleItems = items.data;
}
is evaluated asynchronously. That means you first fire the request, then javascript keeps on executing your lines of code, hence performs
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
At this point the callback was not invoked yet, because this takes some time and can happen at any point. The execution is not stopped for this.
Therefore the value of $scope.formuleItems is still undefined, of course.
After that - at some not defined time in the future (probably a few milliseconds later) the callback will be invoked and the value of $scope.formuleItems will be changed. You have to log the value INSIDE of your callback-function.
You urgently have to understand this concept if you want to succeed in JavaScript, because this happens over and over again :)
In my Angular app, I have some resource modules, each containing some cache factories.
For example,
projectRsrc.factory('getProjectCache', ['$cacheFactory', function($cacheFactory){
return $cacheFactory('getProjectCache');
}]);
I have a few of these to cache values received from the servers.
The problem is that at times I'd like to clear all the caches. So I want to put all the cacheFactories into one CacheCentralApp module and delete all the caches with a single call.
The trouble is, I don't know of any way to access other factories inside my module. So for instance, if I create a module CacheCentralApp, and in it, declare factories that provide cacheFactorys, how can I create a function in there that calls removeAll() on every cacheFactory?
I don't think it is possible to target all the factories of a certain module. I think however that another solution to your problem is to send a event that all factories has to be cleared. This will prevent that you will have to loop through all your factories and call a .clear() function on everyone.
You could send a event request with the following code:
$scope.$broadcast('clearAllCaches');
And listen to this event in every factory with:
$scope.$on('clearAllCaches', function() {
clearCache();
}
In a separate module you might create a factory for that:
var cacheModule = angular.module('CacheCentralApp', []);
cacheModule.factory('MyCacheFactory', ['$cacheFactory', function($cacheFactory) {
var cacheKeys = [];
return {
clearAll: function() {
angular.forEach(cacheKeys, function(key) {
$cacheFactory.get(key).removeAll();
});
},
get: function(key) {
if(cacheKeys.indexOf(key) == -1) {
cacheKeys.push(key);
return $cacheFactory(key);
} else {
return $cacheFactory.get(key);
}
}
}
}]);
To create new or get existing Cache you simply call MyCacheFactory.get(cacheName). To clear all the caches ever created in the factory you call MyCacheFactory.clearAll().
Note: I am not quite sure that Array.indexOf is available in every browser, you might want to use Lo-Dash or another library to make sure your code works.
I'm very new to Jasmine and Marionette and looking for some help on how to test and even just the proper way to think about testing my application. Any pointers are welcome.
I have a Marionette Controller that I use to fetch my model, instantiate my views and render them. I use a method found at the bottom of this page so that the model is fetched before the view is rendered: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported.
My controller method to fetch the model and display the view looks like so:
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
},
As you can see, it calls the showContentView after the model is fetched. That method is here:
showContentView: function(model){
App.views.Body = new bodyView({
model: App.models.Case
});
App.views.Body.on('case:update', this.submitCase, this);
// this.layout is defined in the controller's initialize function
this.layout.content.show(App.views.Body);
},
What is the proper way to test this functionality? I'd like to test the calling of the showContentView function after the completion of the promise. How should I break up the specs for this?
Thanks.
First, spy on your showContentView method and assert it has been called:
it('showCaseById', function (done) {
var controller = new Controller();
spyOn(controller, 'showContentView');
controller.showCaseById('foo');
expect(controller.showContentView).toHaveBeenCalledWith(jasmine.any(caseModel));
});
Secondly, I would recommend you stub out the call to fetch() so you don't hit the network, but it's starting to get a bit hairy now:
function caseModel() {
this.fetch = function () {
// return a promise here that resolves to a known value, e.g. 'blah'
};
}
Now, you can have a slightly stronger assertion, but this is a bit shonky because you're fiddling around with internals of your dependencies:
expect(controller.showContentView).toHaveBeenCalledWith('blah');
By overriding caseModel, when your controller method goes to create one, it gets your new version instead of the old one, and you can control the implementation of the new one just for this test.
There are ways to make this code more testable, but as it seems you're just starting out with testing I won't go into it all. You'll certainly find out those things for yourself as you do more testing.
First, it's important to understand that _.bind(fn, context) doesn't actually call fn. Instead, it returns a function that when called will call fn(). The context defines the object that fn will use internally as this.
It's not necessary but you could write showCaseById as :
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
var fn = _.bind(this.showContentView, this);
$.when(promise).then(fn);
},
As I say, that is unnecessary but now you understand that _.bind() returns a function and that $.when(promise).then(...) accepts a function as its (first) argument.
To answer the actual question, you can confirm that the App.models.Case.fetch() promise has been fulfilled by adding a further $.when(promise).then(...) statement, with a test function of your own choosing.
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
// start: test
$.when(promise).then(function() {
console.log("caseModel " + id + " is ready");//or alert() if preferred
});
// fin: test
},
The second $.when(promise).then(...) will not interfere with the first; rather, the two will execute sequentially. The console.log() satatement will provide reliable confirmation that the this.showContentView has been called successfully and that initial rendering should have happened.
If nothing is rendered at this point or subsequently, then you must suspect that this.showContentView needs to be debugged.