I have a modal dialog where the user can select files to be uploaded. The actual file select/upload is handled by ng-file-upload. When the user selects one or more file, they are added to a list in the dialog, showing progress, completion and failure statuses for each element. The list of items are handled inside a custom directive, since it's used other places as well.
I need to prevent the user from dismissing the dialog while files are still uploading, and that's a challenge for me, cause the button for closing the dialog is in one controller, while the list of uploads is in another (the directive controller). I have solved that by giving and empty list to the directive like this:
//extract from directive:
var directive = {
...
scope: {
'files': '='
}
}
//extract from usage
<uploadFiles files="files" />
Now the outer controller and the inner controller shares the list of files uploading.
So when the user tries to dismiss the dialog by clicking the Close button, I first check if the list contains files still uploading, and if so, I disable the button and display a spinner and a 'please wait'-text.
//from the outer controller
function onModalOk() {
if (uploadInProgress()) {
waitForCompletionBeforeClosingDialog();
} else {
closeDialog();
}
}
the waitForCompletionBeforeClosingDialog() is implemented by setting up a deep watch on the files array. Each time the watch is triggered, I loop through to see if every file has completed. If so, I delete the watch and dismiss the dialog.
function waitForCompletionBeforeClosingDialog() {
$scope.showWaitText = true;
var unregisterWatchForCompletion = $scope.$watch('files', function(files) {
if (allCompleted(files)) {
unregisterWatchForCompletion();
closeDialog();
}
}, true);
}
Everything is working ok, except for one little thing...
In the console, I get this error:
TypeError: Illegal invocation
at equals (angular.js:931)
at equals (angular.js:916)
at Scope.$digest (angular.js:14302)
at Scope.$apply (angular.js:14571)
at angular.js:16308
at completeOutstandingRequest (angular.js:4924)
at angular.js:5312
and it's fired in a tight loop.
I have tried debugging this error, but with no luck..
Do anyone have any ideas?
Is there better ways of doing this all together?
What about using an $httpInterceptor to keep count of the amount of active requests?
something like:
angular.module('someModule').provider('httpStatus', ['$httpProvider', function ($httpProvider) {
var currentRequestCount = 0;
var interceptor = ['$q', function ($q) {
return {
request: function (config) {
currentRequestCount++;
return config;
},
response: function (response) {
currentRequestCount--;
return response;
},
responseError: function (rejection) {
currentRequestCount--;
return $q.reject(rejection);
}
}
}];
$httpProvider.interceptors.push(interceptor);
this.$get = function () {
return {
isWaiting: function () {
return currentRequestLength > 0;
}
}
};
}]);
You could inject the httpStatus service into your dialog and use it to disable the buttons if there are any active requests. May need to add the requestError handler also.
https://docs.angularjs.org/api/ng/service/$http
Related
I'm having some problems with users clicking buttons multiple times and I want to suppress/ignore clicks while the first Ajax request does its thing. For example if a user wants add items to their shopping cart, they click the add button. If they click the add button multiple times, it throws a PK violation because its trying to insert duplicate items into a cart.
So there are some possible solutions mentioned here: Prevent a double click on a button with knockout.js
and here: How to prevent a double-click using jQuery?
However, I'm wondering if the approach below is another possible solution. Currently I use a transparent "Saving" div that covers the entire screen to try to prevent click throughs, but still some people manage to get a double click in. I'm assuming because they can click faster than the div can render. To combat this, I'm trying to put a lock on the Ajax call using a global variable.
The Button
<span style="SomeStyles">Add</span>
Knockout executes this script on button click
vmProductsIndex.AddItemToCart = function (item) {
if (!app.ajaxService.inCriticalSection()) {
app.ajaxService.criticalSection(true);
app.ajaxService.ajaxPostJson("#Url.Action("AddItemToCart", "Products")",
ko.mapping.toJSON(item),
function (result) {
ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
item.InCart(true);
item.QuantityOriginal(item.Quantity());
},
function (result) {
$("#error-modal").modal();
},
vmProductsIndex.ModalErrors);
app.ajaxService.criticalSection(false);
}
}
That calls this script
(function (app) {
"use strict";
var criticalSectionInd = false;
app.ajaxService = (function () {
var ajaxPostJson = function (method, jsonIn, callback, errorCallback, errorArray) {
//Add the item to the cart
}
};
var inCriticalSection = function () {
if (criticalSectionInd)
return true;
else
return false;
};
var criticalSection = function (flag) {
criticalSectionInd = flag;
};
// returns the app.ajaxService object with these functions defined
return {
ajaxPostJson: ajaxPostJson,
ajaxGetJson: ajaxGetJson,
setAntiForgeryTokenData: setAntiForgeryTokenData,
inCriticalSection: inCriticalSection,
criticalSection: criticalSection
};
})();
}(app));
The problem is still I can spam click the button and get the primary key violation. I don't know if this approach is just flawed and Knockout isn't quick enough to update the button's visible binding before the first Ajax call finishes or if every time they click the button a new instance of the criticalSectionInd is created and not truely acting as a global variable.
If I'm going about it wrong I'll use the approaches mentioned in the other posts, its just this approach seems simpler to implement without having to refactor all of my buttons to use the jQuery One() feature.
You should set app.ajaxService.criticalSection(false); in the callback methods.
right now you are executing this line of code at the end of your if clause and not inside of the success or error callback, so it gets executed before your ajax call is finished.
vmProductsIndex.AddItemToCart = function (item) {
if (!app.ajaxService.inCriticalSection()) {
app.ajaxService.criticalSection(true);
app.ajaxService.ajaxPostJson("#Url.Action("AddItemToCart", "Products")",
ko.mapping.toJSON(item),
function (result) {
ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
item.InCart(true);
item.QuantityOriginal(item.Quantity());
app.ajaxService.criticalSection(false);
},
function (result) {
$("#error-modal").modal();
app.ajaxService.criticalSection(false);
},
vmProductsIndex.ModalErrors);
}
}
you could use the "disable" binding from knockout to prevent the click binding of the anchor tag to be fired.
here is a little snippet for that. just set a flag to true when your action starts and set it to false again when execution is finished. in the meantime, the disable binding prevents the user from executing the click function.
function viewModel(){
var self = this;
self.disableAnchor = ko.observable(false);
self.randomList = ko.observableArray();
self.loading = ko.observable(false);
self.doWork = function(){
if(self.loading()) return;
self.loading(true);
setTimeout(function(){
self.randomList.push("Item " + (self.randomList().length + 1));
self.loading(false);
}, 1000);
}
}
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
Click me
<br />
<div data-bind="visible: loading">...Loading...</div>
<br />
<div data-bind="foreach: randomList">
<div data-bind="text: $data"></div>
</div>
I have 2 controllers like below,
app.controller('ParentMenuController',
function ($scope,MenuService) {
$scope.contentLoaded = false;
$scope.showButton = false;
$scope.showButton = MenuService.getStatus();
});
Controller2:
app.controller('ChildMenuController',
function ($scope,MenuService) {
MenuService.setStatus(true);
});
Service:
app.factory('MenuService', function ($q,$http) {
var status= false;
var setStatus=function(newObj){
status=newObj;
};
var getStatus=function(){
return status;
};
return {
getStatus:getStatus,
setStatus:setStatus
};
});
I am not able to set the status to true, but the below line of coding is not at all executing, so the status is always false.
$scope.showButton = MenuService.getStatus();
On button click or any action from user i can trigger the event, but my requirement is while page load, the button should not be visible. When childMenu controller executes, then parent controller button should be visible. I dont want to use $broadcast which requires $rootscope.
Note: My controller and html has hundereds of lines. I just pasted here required code for this functionality. ChildMenuController(childMenu.html) has separate html and ParentMenuController(parentMenu.html) has separete html.
So $scope.showButton is not available in ChildMenucontroller. Both html is used as directive. Main html is index.html.
See this sample:
http://plnkr.co/edit/TepAZGOAZZzQgjUdSpVF?p=preview
You need to use a wrapper object so that it's properties are changed instead of the main object.
app.controller('ParentMenuController',
function ($scope,MenuService) {
$scope.contentLoaded = false;
$scope.showButton = MenuService.getStatus();
});
app.controller('ChildMenuController',
function ($scope,MenuService) {
MenuService.setStatus(true);
});
app.factory('MenuService', function ($q,$http) {
var status= {value:false};
var setStatus=function(newObj){
status.value=newObj;
};
var getStatus=function(){
return status;
};
return {
getStatus:getStatus,
setStatus:setStatus
};
});
It's exactly the same as your code, but state is now an object with a value property. The state object is always the same one so that when the value is changed in the service the changes are propagated to everyone that has ever requested that object.
Actually your service is getting called and this piece of line is executing
$scope.showButton = MenuService.getStatus();
but once your child controller got loaded you are only setting the status but in order to show button you should getStatus after setting it
Like this,
app.controller('ChildMenuController', function($scope, MenuService) {
MenuService.setStatus(true);
$scope.$parent.showButton = MenuService.getStatus();
});
this will set the button to true and it will be shown.
I have done a sample with your code please take a look into it
DEMO
I recently built a custom dashboard with the help of Sebastiaan forums in this post:
http://our.umbraco.org/forum/umbraco-7/using-umbraco-7/60887-Custom-dashboard-Umbraco-update-service
However, I have now modified my code in an attempt to make the interface more user friendly by including a datepicker on two fields so that our users can pass two dates into our web services and have a result returned.
The problem is, I am receiving the following Javascript errors in Firebug when I try and access my Dashboard in the back office:
Error: Argument 'AxumUpdateService' is not a function, got undefined
cb#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:17:79
xa#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:17:187
Jc/this.$gethttp://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:53:310
k/<#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:44:274
n#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:7:72
k#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:44:139
e#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:40:139
y/<#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:39:205
Odhttp://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:158:14
u/j.success/<#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:100:347
Uc/e/j.promise.then/i#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:79:432
Uc/e/j.promise.then/i#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:79:432
Uc/e/j.promise.then/i#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:79:432
Uc/e/j.promise.then/i#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:79:432
Uc/g/<.then/<#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:80:485
Xc/this.$gethttp://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:92:268
Xc/this.$gethttp://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:90:140
Xc/this.$gethttp://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:92:429
j#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:101:78
r#http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:104:449
dd/http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js:106:90
http://localhost:60282/umbraco/lib/angular/1.1.5/angular.min.js Line
63
At first I thought it was due to some of my Javascript causing a conflict but I have checked it and there are no missing semicolons or errors in the code.
I then checked my package.manifest to ensure that Jquery was called before AngularJS as this is advised on most forums however, this still hasn't helped with this issue.
Does anybody know how to circumvent these issues?
package.manifest
{
javascript:[
"~/app_plugins/Dashboards/jquery-1.11.2.min.js,
"~/app_plugins/Dashboards/css/jquery-ui.min.js",
"~/app_plugins/Dashboards/AxumUpdateServiceJquery.js",
"~/app_plugins/Dashboards/AxumUpdateService.controller.js",
"~/app_plugins/Dashboards/AxumUpdateService.service.js",
],
css:[
"~/app_plugins/Dashboards/css/axumupdateservice.min.css",
"~/app_plugins/Dashboards/css/jquery-ui.min.css"
]
}
AxumUpdateService.service.js
angular.module("umbraco.services").factory("AxumUpdateService", function ($http) {
return {
getAll: function (from, to) {
from = from || "";
to = to || "";
return $http.get("/umbraco/api/Axum/GetAllGroupTours" + "?fromDate=" + from + "&toDate=" + to);
}
}
});
AxumUpdateService.controller.js
angular.module("umbraco")
.controller("AxumUpdateService",
function ($scope, $http, AxumUpdateService) {
$scope.getAll = function () {
$scope.load = true;
$scope.info = "Retreiving updates";
AxumUpdateService.getAll($scope.fromDate, $scope.toDate).success(function (data) {
$scope.result = data;
$scope.info = data;
$scope.load = false;
});
};
});
AxumUpdateServiceJquery.js
$(document).ready(function () {
$(".datepicker").datepicker();
});
Not really sure it's the exact same error I've had but what worked for me was to incrementing the clientDependency version in ClientDependency.config to force it to refresh the cached js files.
Maybe not "proper procedure", but it did the trick for me.
I have a login-dialog using a angular-strap modal, which gets invoked by:
scope.authModal = $modal({
template: '/components/login/login.html',
show: false,
scope: scope,
backdrop: 'static'
});
(that code is inside the link function of a login-directive.)
Now, my protractor code looks like this:
it('should perform login properly', function () {
browser.manage().deleteAllCookies();
element(by.model('login.username')).sendKeys('xy123');
element(by.model('login.password')).sendKeys('abz89');
element(by.binding("guiText.loginButton")).click();
browser.waitForAngular();
expect(element(by.id('login.username')).isPresent()).to.eventually.equal(false);
});
In another test above the element(by.id('login.username')).isPresent() has been proved to equal true when the login-dialog is visible.
The problem is, I'm getting Error: timeout of 10000ms exceeded with that test. In the browser I can see, that the credentials are typed in correctly and the button is being clicked. The login modal disappeas and then nothing happens and the browser is eventually running in to that timeout exception after waiting 10 seconds.
I had the same problem and I did below to solve this.
Write this function in your helper file and call this to click on login button in your code. Try to access the button by Id and then pass the id in this function, if not id then update the function as per your need
var clickAndWait= function (btnId) {
var returnVal = false;
browser.wait(function () {
if (!returnVal) {
element(by.id(btnId)).click().then(function () {
returnVal = true;
});
}
return returnVal;
}, 30000);
};
There are menu button ("clients"), tree panel with clients list (sorted by name) and viewer with selected client details. There is also selectionchange action..
My task - on button click switch to client view and select and load details for first client every time button has been clicked. My problem - store is not loaded, how waiting until ext js will autoload data to the store?
my controller code:
me.control({
'#nav-client': {
click: me.onNavClientClick
},
...
'clientlist': {
// load: me.selectClient,
selectionchange: me.showClient
}
});
onNavClientClick: function(view, records) {
var me = this,
content = Ext.getCmp("app-content");
content.removeAll();
content.insert(0, [{xtype: 'clientcomplex'}]);
var first = me.getClientsStore().first();
if (first) {
Ext.getCmp("clientList").getSelectionModel().select(me.getClientsListStore().getNodeById(first.get('clientId')));
}
},
...
Two main questions:
is it good solution in my case? (to select first client in tree panel)
var first = me.getClientsStore().first();
// i use another store to get first record because of i dont know how to get first record (on root level) in TreeStore
...
Ext.getCmp("clientList").getSelectionModel().select(me.getClientsListStore().getNodeById(first.get('clientId')));
i know this code works ok in case of "load: me.selectClient," (but only once),
if i place this code on button click - i see error
Uncaught TypeError: Cannot read property 'id' of undefined
because of me.getClientsListStore() is not loaded.. so how to check loading status of this store and wait some until this store will be completely autoloaded?..
Thank you!
You can listen the store 'load' event. Like this:
...
onNavClientClick: function(view, records) {
var me = this;
// if the store isn't loaded, call load method and defer the 'client view' creation
if (me.getClientsStore.getCount() <= 0) {
me.getClientsStore.on('load', me.onClientsStoreLoad, me, { single : true});
me.getClientsStore.load();
}
else {
me.onClientsStoreLoad();
}
},
onClientsStoreLoad : function () {
var me = this,
content = Ext.getCmp("app-content");
content.removeAll();
content.insert(0, [{xtype: 'clientcomplex'}]);
var first = me.getClientsStore().first();
if (first) {
Ext.getCmp("clientList").getSelectionModel().select(me.getClientsListStore().getNodeById(first.get('clientId')));
}
},
...