I have this application with a bunch of check boxes. Checking/un-checking these boxes will fire an $http.post() to the server. I want it to now fire 1 post as soon as the user is done clicking, but I'm not sure how to set up my $timeout.
I have a plunker set up here: https://plnkr.co/edit/QJRc9uMxJA17eFszyexO
What this currently does is that when the user checks a checkbox, a single alert is fired after 1 sec. (Im using the alert to simulate the $http.post().
Is there a way that I can reset that $timeout when the user clicks on a new check box? So if I click on 5 checkboxes continuously, it will fire 1 alert rather than 5?
$timeout() returns a promise (you can call .then() on it, but that's another story). Also there is $timeout.cancel(promise) to cancel a pending timeout. So modify your code as:
app.controller('IndexController', function($scope, $timeout) {
var prevTimeout;
$scope.onClick = function() {
if( prevTimeout ) {
$timeout.cancel(prevTimeout);
}
prevTimeout = $timeout(function() {
prevTimeout = null;
alert('some http post called.');
}, 1000);
};
$scope.model = new Array(5);
});
I think what you need is debounce functionality. If you are using lodasd check https://lodash.com/docs#debounce
Related
I am working for an application and I want to communicate Angular js with a third party js library. For this I have used pubsub method using mediator js. But due to this when I subscribe to any event then it subscribe multiple times and due to this when I publish event, it fires multiple times.
I have used below code:
angular.module('app')
.service('mediator', function() {
var mediator = window.mediator || new Mediator();
return mediator;
});
// Main controller begins here
angular.module('app').controller('MainController', MainController);
MainController.$inject = ['mediator'];
function MainController(mediator){
var vm = this;
vm.title = "This is main controller."
vm.sendMessage = function(){
mediator.publish('something', { data: 'Some data' });
}
}
// First page controller begins here
angular.module('app').controller('FirstController', FirstController);
FirstController.$inject = ['mediator'];
function FirstController(mediator){
var vm = this;
console.log('Subscribed events for first controller.');
var counter = 0;
mediator.subscribe('something', function(data){
console.log('Fired event for '+ counter.toString());
counter = counter + 1;
});
}
Here is the plunker for better explanation:
Plunkr
To test this plunker:
Run plunker.
Open developer console.
Click on First page
Click fire event
Click on second page
Click on first page
Click on fire event
As you navigate to first page second time, it will subscribe for event again and will fire twice. This will subscribe multiple time when you navigate to first page multiple times.
Please let me know if I am doing something wrong.
You could unsubscribe when the controller is destroyed.
To do this using mediator, you first need to save subscription function:
var sub = function(data){
console.log('Fired event for '+ counter.toString());
counter = counter + 1;
}
mediator.subscribe('something', sub);
Then you can use the angular event to unsubscribe from the notifications when the controller is removed:
$scope.$on("$destroy", function() {
mediator.remove("something", sub);
});
Whenever using this pattern, you should consider the moments when a subscription needs to be removed, not only for duplication reasons, but also it can cause memory leaks.
Don't forget you also need to inject $scope (even if not using it as a holder of model, it's fine to use it for registering event listeners):
angular.module('app').controller('FirstController', FirstController);
FirstController.$inject = ['mediator', '$scope'];
Plunkr example: https://plnkr.co/edit/CgYLLSxGF2Fww5vBB7PB
Hope it helps.
I have form with a submit method. Inside the form there is an input tag with a blur event handler. User types in some text into the input to look up item and clicks button to submit form. Below is some pseudocode:
MethodToGetData
{
call http to get data and setup model objects
}
Blur Event Handler
{
MethodToGetData
}
Submit Method
{
MethodToGetData
AddItem
}
The issue I am running into is if the user types in text and immediately clicks button to execute Submit, the blur event handler gets executed first and makes the http call. The submit method also makes the http call. I want to be able to execute the http call only once.
Any suggestions/thoughts on how to handle this?
Thanks
Use a promise. Inject $q into your controller.
function controllerConstructor($q, someService) {
var vm = this;
var promise;
vm.blur = function() {
var deferred = $q.defer();
promise = deferred.promise;
someService.httpMethod().then(function() {
deferred.resolve(dataToPass);
});
};
vm.submit = function() {
promise.then(
function(dataThatWasPassed) {
// Wont run until http call is finished
}
);
};
}
I'm having trouble with a protractor test.
Overview of the test
Click button to show a form
Fill out the form
Click another button to save the form - this should call a function in one of my controllers that makes an http call and then reloads some models on the page.
When clicking the final "save" button, everything seems to freeze and the attached ng-click function never seems to get called. I don't see any errors in the console when I use browser.pause. I can see that the button is in fact clicked, but at that point, nothing seems to happen.
Final button definition:
this.addConfigFilterLineButton = element(by.css('[ng-click="cflCtrl.create();"]'));
Function that fills out the form:
this.addNewConfigFilterLine = function (cb) {
var self = this;
var deferred = protractor.promise.defer();
browser.executeScript('window.scrollTo(0,10000);')
.then(function(){
self.newConfigFilterLineButton.click();
// code that fills out the form
self.addConfigFilterLineButton.click();
browser.waitForAngular()
.then(function(){
deferred.fulfill();
});
});
return deferred.promise;
};
Spec
it('should allow creating a new ConfigFilterLine', function (done) {
var length;
settingsPage.configFilterLines.count()
.then(function(count){
length = count;
return settingsPage.addNewConfigFilterLine();
})
.then(function(){
expect(settingsPage.configFilterLines.count()).to.eventually.equal(length+1);
done();
});
});
I've tried with browser.waitForAngular and without it, and it doesn't seem to matter. When the button is clicked, nothing happens.
Any ideas would be helpful. Let me know if there's more info I can provide.
Instead of using
this.addConfigFilterLineButton = element(by.css('[ng-click="cflCtrl.create();"]'));
try something more like this:
this.addConfigFilterLineButton = element(by.id('id-of-final-button'));
My guess is that Protractor isn't correctly finding "addConfigFilterLineButton".
My user types something. I want to save the typed data after every say 1000ms the user stopped typing.
I do NOT want to use $intervall with 1000ms.
How can I do this using latest angular 1.3.11 ?
Using Angular 1.3
<input ng-model='data' ng-model-options="{ debounce: 1000 }" />
Further information on ng-model-options in the angular docs
In your controller, you can $watch('data') and save your changes whenever data is changed (just like you would if you weren't debouncing the input).
You may also be able to use ng-change instead of $watch, but I haven't tested how that interacts with debounce (but based on other answers, looks like it would work as expected).
In this case, use the change event and the typical way of debouncing an event.
Template:
<input type="text" ng-change="valueChanged()" ng-model="value">
Controller:
$scope.value = '';
var timeout, delay = 1000;
$scope.valueChanged = function () {
if (timeout) {
$timeout.cancel(timeout);
}
timeout = $timeout(function () {
// do something
}, delay);
};
don't forget to pass $timeout to your controller. What this will do is every time that event is triggered, the previous timeout will be canceled and a new one will start. when the event doesn't happen for delay milliseconds, it will complete and call doSomething.
Use $timeout to schedule the service call after 1 second when the user types something. Cancel the previous $timeout first.
var promise;
$scope.userTypedSomething = function() {
if (promise) {
$timeout.cancel(promise);
}
promise = $timeout(service.method, 1000);
}
Another option is to combine the user of getterSetter and debounce in ngModelOptions, so that a setter, calling the servicemethod, is called after 1 second of inactivity.
I'm trying to test the login page on my site using protractor.
If you log in incorrectly, the site displays a "toast" message that pops up for 5 seconds, then disappears (using $timeout).
I'm using the following test:
describe('[login]', ()->
it('should show a toast with an error if the password is wrong', ()->
username = element(select.model("user.username"))
password = element(select.model("user.password"))
loginButton = $('button[type=\'submit\']')
toast = $('.toaster')
# Verify that the toast isn't visible yet
expect(toast.isDisplayed()).toBe(false)
username.sendKeys("admin")
password.sendKeys("wrongpassword")
loginButton.click().then(()->
# Verify that toast appears and contains an error
toastMessage = $('.toast-message')
expect(toast.isDisplayed()).toBe(true)
expect(toastMessage.getText()).toBe("Invalid password")
)
)
)
The relevant markup (jade) is below:
.toaster(ng-show="messages.length")
.toast-message(ng-repeat="message in messages") {{message.body}}
The problem is the toastMessage test is failing (it can't find the element). It seems to be waiting for the toast to disappear and then running the test.
I've also tried putting the toastMessage test outside the then() callback (I think this is pretty much redundant anyway), but I get the exact same behaviour.
My best guess is that protractor sees that there's a $timeout running, and waits for it to finish before running the next test (ref protractor control flow). How would I get around this and make sure the test runs during the timeout?
Update:
Following the suggestion below, I used browser.wait() to wait for the toast to be visible, then tried to run the test when the promise resolved. It didn't work.
console.log "clicking button"
loginButton.click()
browser.wait((()-> toast.isDisplayed()),20000, "never visible").then(()->
console.log "looking for message"
toastMessage = $('.toaster')
expect(toastMessage.getText()).toBe("Invalid password")
)
The console.log statements let me see what's going on. This is the series of events, the [] are what I see happening in the browser.
clicking button
[toast appears]
[5 sec pass]
[toast disappears]
looking for message
[test fails]
For added clarity on what is going on with the toaster: I have a service which essentially holds an array of messages. The toast directive is always on the page (template is the jade above), and watches the messages in the toast service. If there is a new message, it runs the following code:
scope.messages.push(newMessage)
# set a timeout to remove it afterwards.
$timeout(
()->
scope.messages.splice(0,1)
,
5000
)
This pushes the message into the messages array on the scope for 5 seconds, which is what makes the toast appear (via ng-show="messages.length").
Why is protractor waiting for the toast's $timeout to expire before moving on to the tests?
I hacked around this using the below code block. I had a notification bar from a 3rd party node package (ng-notifications-bar) that used $timeout instead of $interval, but needed to expect that the error text was a certain value. I put used a short sleep() to allow the notification bar animation to appear, switched ignoreSynchronization to true so Protractor wouldn't wait for the $timeout to end, set my expect(), and switched the ignoreSynchronization back to false so Protractor can continue the test within regular AngularJS cadence. I know the sleeps aren't ideal, but they are very short.
browser.sleep(500);
browser.ignoreSynchronization = true;
expect(page.notification.getText()).toContain('The card was declined.');
browser.sleep(500);
browser.ignoreSynchronization = false;
It turns out that this is known behaviour for protractor. I think it should be a bug, but at the moment the issue is closed.
The workaround is to use $interval instead of $timeout, setting the third argument to 1 so it only gets called once.
you should wait for your toast displayed then do other steps
browser.wait(function() {
return $('.toaster').isDisplayed();
}, 20000);
In case anyone is still interested, this code works for me with no hacks to $timeout or $interval or Toast. The idea is to use the promises of click() and wait() to turn on and off synchronization. Click whatever to get to the page with the toast message, and immediately turn off sync, wait for the toast message, then dismiss it and then turn back on sync (INSIDE the promise).
element(by.id('createFoo')).click().then(function () {
browser.wait(EC.stalenessOf(element(by.id('createFoo'))), TIMEOUT);
browser.ignoreSynchronization = true;
browser.wait(EC.visibilityOf(element(by.id('toastClose'))), TIMEOUT).then(function () {
element(by.id('toastClose')).click();
browser.ignoreSynchronization = false;
})
});
I hope this can help who has some trouble with protractor, jasmine, angular and ngToast.
I create a CommonPage to handle Toast in every pages without duplicate code.
For example:
var CommonPage = require('./pages/common-page');
var commonPage = new CommonPage();
decribe('Test toast', function(){
it('should add new product', function () {
browser.setLocation("/products/new").then(function () {
element(by.model("product.name")).sendKeys("Some name");
var btnSave = element(by.css("div.head a.btn-save"));
browser.wait(EC.elementToBeClickable(btnSave, 5000));
btnSave.click().then(function () {
// this function use a callback to notify
// me when Toast appears
commonPage.successAlert(function (toast) {
expect(toast.isDisplayed()).toBe(true);
});
});
});
})
});
And this is my CommonPage:
var _toastAlert = function (type, cb) {
var toast = null;
switch (type) {
case "success":
toast = $('ul.ng-toast__list div.alert-success');
break;
case "danger":
toast = $('ul.ng-toast__list div.alert-danger');
break;
}
if (!toast) {
throw new Error("Unable to determine the correct toast's type");
}
browser.ignoreSynchronization = true;
browser.sleep(500);
browser.wait(EC.presenceOf(toast), 10000).then(function () {
cb(toast);
toast.click();
browser.ignoreSynchronization = false;
})
}
var CommonPage = function () {
this.successAlert = function (cb) {
_toastAlert("success", cb);
};
this.dangerAlert = function(cb) {
_toastAlert("danger", cb);
}
}
module.exports = CommonPage;
Chris-Traynor's answer worked for me but i've got an update.
ignoreSynchronization is now deprecated.
For those using angular and protractor to test this, the below works nicely for me.
$(locators.button).click();
await browser.waitForAngularEnabled(false);
const isDisplayed = await $(locators.notification).isPresent();
await browser.waitForAngularEnabled(true);
expect(isDisplayed).toEqual(true);
I've simplified this to make it easier to see, I would normally place this inside a method to make the locators dynamic.