Unregistering backbone events - javascript

I'm in a real bottleneck with backbone.
I'm new to it, so sorry if my questios are stupid, as i probably didn't get the point of the system's structure.
Basically, I'm creating ad application which lets you do some things for different "steps". Therefore, I've implemented some kind of pagination system. Each time a page sasisfies certain conditions, the next page link is shown, and the current page is cached.
Each page uses the same "page" object model/view, and the navigation is appended there each time. it's only registered one time anyway, and I undelegate/re-delegate events as the old page fades out and the new one fades in.
If I always use cached versions for previous pages, everything is okay. BUT, if I re-render a page that was already rendered, when I click "go next page", it skips ahead of how many times i re-rendered the page itself.
it's like the "go next page" button has been registered, say, 3 times, and was never removed from the events listener.
It's a very long application in terms of code, and i hope you can understand the basica idea, and give me some hints, without needing to have the full code here.
Thanks in advance, i hope somebody can help me out since i'm in a real bottleneck!
p.s. for some reason, I've noticed that the next/previous buttons respective html is not cached within the page. Weird.
---UPDATE----
I tried the stopListening suggestion, but it didn't work. Here is jmy troublesome button:
// Register the next button
App.Views.NavNext = Backbone.View.extend({
el: $('#nav-next'),
initialize: function() {
vent.on('showNext', function() {
this.$el.fadeIn();
}, this)
},
events: {
'click': 'checkConditions'
},
checkConditions: function() {
//TODO this is running 2 times too!
console.log('checking conditions');
if (current_step == 1)
this.go_next_step();
vent.trigger('checkConditions', current_step); // will trigger cart conditions too
},
go_next_step: function() {
if(typeof(mainView.stepView) != 'undefined')
{
mainView.stepView.unregisterNavigation();
}
mainView.$el.fadeOut('normal', function(){
$(this).html('');
current_step++;
mainView.renderStep(current_step);
}); //fadeout and empty, then refill
}
});
Basically, checkConditions runs 2 times as if the previousle rendered click is still registered. Here is where it's being registered, and then unregistered after the current step fades off (just a part of that view!):
render: function() {
var step = this;
//print the title for this step
this.$el.attr('id', 'step_' + current_step);
this.$el.html('<h3>'+this.model.get('description')+'</h3>');
// switch display based on the step number, will load all necessary data
// this.navPrev = new App.Views.NavPrev();
// this.navNext = new App.Views.NavNext();
this.$el.addClass('grid_7 omega');
// add cart (only if not already there)
if (!mainView.cart)
{
mainView.cart = new App.Models.Cart;
mainView.cartView = new App.Views.Cart({model: mainView.cart})
mainView.$el.before(mainView.cartView.render().$el)
}
switch (this.model.get('n'))
{
case 5: // Product list, fetch and display based on the provious options
// _.each(mainView.step_values, function(option){
// console.log(option)
// }, this);
var products = new App.Collections.Products;
products.fetch({data:mainView.step_values, type:'POST'}).complete(function() {
if (products.length == 0)
{
step.$el.append('<p>'+errorMsgs['noprod']+'</p>')
}
else {
step.contentView = new App.Views.Products({collection: products});
step.$el.append(step.contentView.render().$el);
}
step.appendNavigation();
});
break;
}
//console.log(this.el)
return this;
},
appendNavigation: function(back) {
if(current_step != 2)
this.$el.append(navPrev.$el.show());
else this.$el.append(navPrev.$el.hide());
this.$el.append(navNext.$el.hide());
if(back) navNext.$el.show();
navPrev.delegateEvents(); // re-assign all events
navNext.delegateEvents();
},
unregisterNavigation: function() {
navNext.stopListening(); // re-assign all events
}
And finally, here is the main view's renderStep, called after pressing "next" it will load a cached version if present, but for the trouble page, I'm not creating it
renderStep : function(i, previous) { // i will be the current step number
if(i == 1)
return this;
if(this.cached_step[i] && previous) // TODO do not render if going back
{ // we have this step already cached
this.stepView = this.cached_step[i];
console.log('ciao'+current_step)
this.stepView.appendNavigation(true);
if ( current_step == 3)
{
_.each(this.stepView.contentView.productViews, function(pview){
pview.delegateEvents(); //rebind all product clicks
})
}
this.$el.html(this.stepView.$el).fadeIn();
} else {
var step = new App.Models.Step({description: steps[i-1], n: i});
this.stepView = new App.Views.Step({model: step})
this.$el.html(this.stepView.render().$el).fadeIn(); // refill the content with a new step
mainView.cached_step[current_step] = mainView.stepView; // was in go_next_step, TODO check appendnavigation, then re-render go next step
}
return this;
}

Try using listenTo and stopListening when you are showing or removing a certain view from the screen.
Take a look at docs: http://backbonejs.org/#Events-listenTo
All events are binded on initialization of the view and when you are removing the view from the screen then unbind all events.
Read this for detailed analysis: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

Related

How do you repeatedly click two buttons when you have to wait for the second button to appear?

I am working on a simple script that loops through all elements on a page and shares them in vanilla JavaScript. Here is what I have so far
var buttons = document.getElementsByClassName('share-gray-large');
for(var i=0; i<buttons.length; i++){
buttons[i].click();
document.getElementsByClassName('internal-share__link')[0].click();
}
The share-gray-large button is the class of the "share" buttons. Once the first share button is clicked, a modal appears that asks the user where they want to share the items to. I need to click the first item in the modal with class name internal-share__link. The problem that I am running up against is the fact that the last line of my code results in the following error
Uncaught TypeError: Cannot read property 'click' of undefined
which makes sense, as the modal hasn't appeared yet at the time of the second click() function being called. I need to wait for the element to appear, then click it, then wait until the modal disappears to share the next item. I've looked into async/await functions, setTimeout(), and the solutions from similar StackOverflow questions. I adapted this secondary solution
var waitForEl = function(className, callback) {
if (document.getElementsByClassName(className).length) {
callback();
} else {
setTimeout(function() {
waitForEl(className, callback);
}, 100);
}
};
var buttons = document.getElementsByClassName('share-gray-large');
for(var i=0; i< buttons.length; i++){
waitForEl('share-gray-large', function() {
document.getElementsByClassName('share-gray-large')[i].click();
});
waitForEl('internal-share__link', function() {
document.getElementsByClassName('internal-share__link')[0].click();
});
}
which kind of works, but I believe that it actually ends up sharing the last item multiple times instead of sharing all of them in order. I ran into this issue of needing to wait for a button to appear as well with a different project, so any help would be greatly appreciated!
TL;DR I'm working on projects with the following sequence of steps. Using a page with 3 items that need to be shared:
Click "share" on first button
Wait for confirmation button to appear in a modal
Click confirm
Wait for modal to disappear
Click "share" on second button
Repeat steps 2-4
Click "share" on third button
Repeat steps 2-4
How do you do this in VanillaJS?
This will click from the last to first share button, email share exluded and make sure the browser is allowed to click/open multiple popup.
function waitForElement(selector) {
var element = document.querySelectorAll(selector);
if (element.length) {
if (shareLinkCount == 999) { // set to real number of elements
shareLinkCount = element.length;
}
shareLinkCount--;
var shareElement = element[shareLinkCount];
if(shareElement.textContent != "Email") // Do not click email share
element[shareLinkCount].click();
if (shareLinkCount) { // not 0
setTimeout(clickShareButton, 500);
}
else{
alert('Click Finished');
document.body.click();
}
} else {
setTimeout(waitForElement, 500, selector);
}
}
function clickShareButton() {
var button = document.querySelector('.share-gray-large');
button.click();
waitForElement('internal-share__link');
}
var shareLinkCount = 999; // dummy number
clickShareButton();
This is the answer that worked for me. Credit goes to #uingtea for most of the solution. Since I did not post the link to the website, I was able to test it and modify their solution to suit my needs.
function waitForElement(selector) {
var element = document.querySelector(selector);
if (element) {
shareLinkCount--;
element.click();
if (shareLinkCount) { // not 0
setTimeout(clickShareButton, 500);
}
else{
element.click();
alert('Click Finished');
}
} else {
setTimeout(waitForElement, 500, selector);
}
}
function clickShareButton() {
document.querySelectorAll('.share-gray-large')[shareLinkCount].click();
waitForElement('.internal-share__link');
}
var shareLinkCount = document.querySelectorAll('.share-gray-large').length - 1;
clickShareButton();

JQuery $.post callback firing a function that never finishes

Here's the problem. I'm making a callback to the server that receives an MVC partial page. It's been working great, it calls the success function and all that. However, I'm calling a function after which iterates through specific elements:
$(".tool-fields.in div.collapse, .common-fields div.collapse").each(...)
Inside this, I'm checking for a specific attribute (custom one using data-) which is also working great; however; the iterator never finishes. No error messages are given, the program doesn't hold up. It just quits.
Here's the function with the iterator
function HideShow() {
$(".tool-fields.in div.collapse, .common-fields div.collapse").each(function () {
if (IsDataYesNoHide(this)) {
$(this).collapse("show");
}
else
$(this).collapse("hide");
});
alert("test");
}
Here's the function called in that, "IsDataYesNoHide":
function IsDataYesNoHide(element) {
var $element = $(element);
var datayesnohide = $element.attr("data-yes-no-hide");
if (datayesnohide !== undefined) {
var array = datayesnohide.split(";");
var returnAnswer = true;
for (var i in array) {
var answer = array[i].split("=")[1];
returnAnswer = returnAnswer && (answer.toLowerCase() === "true");
}
return returnAnswer;
}
else {
return false;
}
}
This is the way the attribute appears
data-yes-no-hide="pKanban_Val=true;pTwoBoxSystem_Val=true;"
EDIT: Per request, here is the jquery $.post
$.post(path + conPath + '/GrabDetails', $.param({ data: dataArr }, true), function (data) {
ToggleLoader(false); //Page load finished so the spinner should stop
if (data !== "") { //if we got anything back of if there wasn't a ghost record
$container.find(".container").first().append(data); //add the content
var $changes = $("#Changes"); //grab the changes
var $details = $("#details"); //grab the current
SplitPage($container, $details, $changes); //Just CSS changes
MoveApproveReject($changes); //Moves buttons to the left of the screen
MarkAsDifferent($changes, $details) //Adds the data- attribute and colors differences
}
else {
$(".Details .modal-content").removeClass("extra-wide"); //Normal page
$(".Details input[type=radio]").each(function () {
CheckOptionalFields(this);
});
}
HideShow(); //Hide or show fields by business logic
});
For a while, I thought the jquery collapse was breaking, but putting the simple alert('test') showed me what was happening. It just was never finishing.
Are there specific lengths of time a callback function can be called from a jquery postback? I'm loading everything in modal views which would indicate "oh maybe jquery is included twice", but I've already had that problem for other things and have made sure that it only ever includes once. As in the include is only once in the entire app and the layout is only applied to the main page.
I'm open to any possibilities.
Thanks!
~Brandon
Found the problem. I had a variable that was sometimes being set as undefined cause it to silently crash. I have no idea why there was no error message.

jquery .on accumulates click events

I have a component wich manages a view that contains arcticles with games and to preventing overload of memory and spend time using a for with every article if it is open on a new window or reloads the current page (with a checkbox), I made this code, when the user clicks on an article(each one has the class "flashgame"), depending of user's choice it will go to a game and, or reload the current page, or, open in another window and play the game
this is my code :
jQuery(document).on('click', '.flashgame', function () {
console.log(this);
let denom = jQuery(this).find('.urlGame').data('denom');
let val = jQuery(this).find('.urlGame').data('val');
if (denom != "") {
let gamear = denom.toString().split(',');
let orderset: any[] = gamear;
if (orderset.length > 1) {
that.multidenoms = orderset.sort((a, b) => { return a - b; });
that.gameId = val;
console.log("modalflash is showing");
that.modalflash.show();
}
else {
that.goToFlash(val, gamear);
}
}
else {
that.goToFlash(val);
}
});
and this is the article:
but for any reason when I made this change in the project, the page has been accumulate the number of clicks, for instance :
Hard reload and the number of "clicks" starts in zero, i go to another page and then go back, the click has incremented by one, an so on.
So, what should I do?, Is there any workaround to prevent this "clicks" overload?
What I like to do is use .off before .on when rebinding event handlers.
So something like this:
jQuery(document).off('click', '.flashgame').on('click', '.flashgame', function () { ...

Wait until ajax request is completed

I have a form with several comboboxes within a window.
If I display the window and close it immediately (with a button close method), sometimes I have poor connection to the server and the request to load the data in comboboxes is interrupted.
Response is "Failed to load response data".
Sometimes, the same happens when a combobox is expanded and the store has not yet been loaded.
For these cases, in my Application.js file I have the following function which displays an error message.
Ext.util.Observable.observe(Ext.data.Connection, {
requestexception: function (connection, response, options) {
Ext.Ajax.abort(store.operation.request);
Ext.Msg.show({
title: 'Error!',
msg: 'Message...',
icon: Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
}
});
I'm trying to prevent the window from being closed until the requests were completed and the data was loaded into the comboboxs.
I do not want to use setTimeout().
Maybe use a mask in window and do the unmask when the request is completed ou disabled/enable de close button.
I appreciated suggestions for finding a solution to this.
EDITED:
Another possibility, probably simpler, is to iterate through all the combobox of the form and check if, in each combobox, the store.isLoading (): If yes, it displays a message to wait until the load is finished.
EDITED
If the form has only one combobox the following handler seems to solve the problem: it creates an initial mask and unmasks it after the store is loaded:
handler: function (btn) {
var win = Ext.widget('winSearch', {
animateTarget: this
}).showBy(this, 'bl');
win.getEl().mask('Loading...');
var store = Ext.getStore('storeCombo1Id');
if(store.isLoading()){
store.on('load', function() {
win.getEl().unmask();
});
}else{
win.getEl().unmask();
}
}
The problem is to iterate through several combobox: I tried the following code, without success (the stores are in a viewmodel):
handler: function (btn) {
var win = Ext.widget('winSearch', {
animateTarget: this
}).showBy(this, 'bl');
win.getEl().mask('Loading...');
// var store1 = Ext.getStore('storeCombo1Id');
// var store2 = Ext.getStore('storeCombo2Id');
// var store3 = Ext.getStore('storeCombo3Id');
// var allComboboxStores = [store1, store2, store3];
var allComboboxStores = ['storeCombo1Id', 'storeCombo2Id', 'storeCombo3Id'];
Ext.each(allComboboxStores, function(storeId) {
var store = Ext.getStore(storeId);
console.log(store); //console show 3 stores
if(store.isLoading()){
store.on('load', function() {
win.getEl().unmask();
});
}else{
win.getEl().unmask();
}
});
}
The problem with this solution is that if the store of one of the comboboxs is loaded it triggers the unmask method independently of other comboboxs still to be loaded.
How to wait until all stores are loaded?
EDITED
I have tried different types of iterations and loops and the following solution seems to work.
handler: function () {
var win = Ext.widget('mywindow', {
animateTarget: this
}).showBy(this, 'bl');
win.getEl().mask('Loading...');
var allComboboxStores = ['storeCombo1Id', 'storeCombo2Id', 'storeCombo3Id'];
var indexStores = 0;
Ext.each(allComboboxStores, function(storeId) {
var store = Ext.getStore(storeId);
if(store){
if(store.isLoading()){
indexStores++
store.on('load', function() {
indexStores--;
if (indexStores == 0){
win.getEl().unmask();
}
});
}
else if(!store.isLoading() && indexStores == 0){
win.getEl().unmask();
}
}
});
}
I appreciated suggestions to improve this solution or suggestions to do otherwise.
If jQuery is not a problem ... I suggest using Promises
Description: Return a Promise object to observe when all actions of a certain type bound to the collection, queued or not, have finished.

Click all anchor tags on page with given class, but cancel prior to navigation

Trying to automate some testing for some analytics tracking code, and I'm running into issues when I try passing links into the each() method.
I copied a lot of this from stackoverflow - how to follow all links in casperjs, but I don't need return the href of the link; I need to return the link itself (so I can click it). I keep getting this error: each() only works with arrays. Am I not returning an array?
UPDATE:
For each anchor tag that has .myClass, click it, then return requested parameters from casper.options.onResourceReceived e.g. event category, event action, etc. I may or may not have to cancel the navigation the happens after the click; I simply only need to review the request, and do not need the follow page to load.
Testing steps:
click link that has .myClass
look at request parameters
cancel the click to prevent it from going to the next page.
I'm new to javascript and casper.js, so I apologize if I'm misinterpreting.
ANOTHER UPDATE:
I've updated the code to instead return an array of classes. There are a few sketchy bits of code in this though (see comments inline).
However, I'm now having issues canceling the navigation after the click. .Clear() canceled all js. Anyway to prevent default action happening after click? Like e.preventDefault();?
var casper = require('casper').create({
verbose: true,
logLevel: 'debug'
});
casper.options.onResourceReceived = function(arg1, response) {
if (response.url.indexOf('t=event') > -1) {
var query = decodeURI(response.url);
var data = query.split('&');
var result = {};
for (var i = 0; i < data.length; i++) {
var item = data[i].split('=');
result[item[0]] = item[1];
}
console.log('EVENT CATEGORY = ' + result.ec + '\n' +
'EVENT ACTION = ' + result.ea + '\n' +
'EVENT LABEL = ' + decodeURIComponent(result.el) + '\n' +
'REQUEST STATUS = ' + response.status
);
}
};
var links;
//var myClass = '.myClass';
casper.start('http://www.leupold.com', function getLinks() {
links = this.evaluate(function() {
var links = document.querySelectorAll('.myClass');
// having issues when I attempted to pass in myClass var.
links = Array.prototype.map.call(links, function(link) {
// seems like a sketchy way to get a class. what happens if there are multiple classes?
return link.getAttribute('class');
});
return links;
});
});
casper.waitForSelector('.myClass', function() {
this.echo('selector is here');
//this.echo(this.getCurrentUrl());
//this.echo(JSON.stringify(links));
this.each(links, function(self, link) {
self.echo('this is a class : ' + link);
// again this is horrible
self.click('.' + link);
});
});
casper.run(function() {
this.exit();
});
There are two problems that you're dealing with.
1. Select elements based on class
Usually a class is used multiple times. So when you first select elements based on this class, you will get elements that have that class, but it is not guaranteed that this will be unique. See for example this selection of element that you may select by .myClass:
myClass
myClass myClass2
myClass myClass3
myClass
myClass myClass3
When you later iterate over those class names, you've got a problem, because 4 and 5 can never be clicked using casper.click("." + links[i].replace(" ", ".")) (you need to additionally replace spaces with dots). casper.click only clicks the first occurrence of the specific selector. That is why I used createXPathFromElement taken from stijn de ryck to find the unique XPath expression for every element inside the page context.
You can then click the correct element via the unique XPath like this
casper.click(x(xpathFromPageContext[i]));
2. Cancelling navigation
This may depend on what your page actually is.
Note: I use the casper.test property which is the Tester module. You get access to it by invoking casper like this: casperjs test script.js.
Note: There is also the casper.waitForResource function. Have a look at it.
2.1 Web 1.0
When a click means a new page will be loaded, you may add an event handler to the page.resource.requested event. You can then abort() the request without resetting the page back to the startURL.
var resourceAborted = false;
casper.on('page.resource.requested', function(requestData, request){
if (requestData.url.match(/someURLMatching/)) {
// you can also check requestData.headers which is an array of objects:
// [{name: "header name", value: "some value"}]
casper.test.pass("resource passed");
} else {
casper.test.fail("resource failed");
}
if (requestData.url != startURL) {
request.abort();
}
resourceAborted = true;
});
and in the test flow:
casper.each(links, function(self, link){
self.thenClick(x(link));
self.waitFor(function check(){
return resourceAborted;
});
self.then(function(){
resourceAborted = false; // reset state
});
});
2.2 Single page application
There may be so many event handlers attached, that it is quite hard to prevent them all. An easier way (at least for me) is to
get all the unique element paths,
iterate over the list and do every time the following:
Open the original page again (basically a reset for every link)
do the click on the current XPath
This is basically what I do in this answer.
Since single page apps don't load pages. The navigation.requested and page.resource.requested will not be triggered. You need the resource.requested event if you want to check some API call:
var clickPassed = -1;
casper.on('resource.requested', function(requestData, request){
if (requestData.url.match(/someURLMatching/)) {
// you can also check requestData.headers which is an array of objects:
// [{name: "header name", value: "some value"}]
clickPassed = true;
} else {
clickPassed = false;
}
});
and in the test flow:
casper.each(links, function(self, link){
self.thenOpen(startURL);
self.thenClick(x(link));
self.waitFor(function check(){
return clickPassed !== -1;
}, function then(){
casper.test.assert(clickPassed);
clickPassed = -1;
}, function onTimeout(){
casper.test.fail("Resource timeout");
});
});

Categories