I have written some code that changes an input quantity on a magento 1.9 ecommerce website.
jQuery("input.qty").val("10");
The problem is the javascript that triggers the total to update doesn't fire. I have found the code responsible and it looks like this:
(function() {
var qtyFields = $('super-product-list').select('input.qty');
qtyFields.each(function(el) {
el.observe("change", updateGroupedPrice);
});
Event.observe(window, "load", updateGroupedPrice);
function updateGroupedPrice() {
//do stuff
}
})();
I think this is using prototype.js but I tried to isolate it in a codepen but couldn't get it working.
I have tried to trigger the change event like so:
jQuery("input.qty").trigger("change")
But it does not work. I also ran through a load of other events but in the dev tools it shows the code listening on "change".
Does anyone know why I can't trigger the change?
Since the page is using Prototype.js, you ought to keep using that to trigger your change. If you introduce jQuery into this, you're a) loading another complete duplicate of what Prototype already does, and b) asking for a lot of trouble isolating the fact that $() is a method in both libraries.
Your jQuery is a little fishy to me, too. You're setting the value of one picker (I imagine) and yet you are addressing it with a classname, so potentially there is more than one select.qty in the page, and all of them will change to value 10, firing off (potentially) multiple callback functions.
The Prototype code you see here is setting up a "listener" for changes on what you would address in jQuery as$(#super-product-list input.qty) inputs.
jQuery always treats $() as returning an array of objects, and thus all of its methods act on the array, even if it only contains one member. Prototype has two different methods for accessing elements in the DOM: $('some_id'), which always returns one element (or none, if no match), and $$('some css selector'), which always returns an array (of zero or more matching elements). You would write (or use native) callback methods differently, depending on which accessor you used to gather the element(s).
If you want to change one of these inputs, you will need to isolate it before you set its value.
Let's say there are three select pickers with the classname qty in your #super-product-list element. You want to change the third one to 10:
$('super-product-list').select('input.qty').last().setValue('10');
Or, much smarter than this, you add an ID to the third one, and then your code is much shorter:
$('quantity_3').setValue('10');
In either case, this will send the "change" event from your select, and the updateGroupedPrice method will observe that and do whatever you have coded it to do.
You won't need to (and should not ever) trigger the change event -- that's a "native" event, and the browser owns it. jQuery's trigger() (which is fire() in Prototype, is used exclusively for "synthetic events", like you see in Bootstrap: show.bs.modal, hide.bs.modal, etc. You can spot these by the punctuation in their names; usually dots or colons to namespace the events and avoid collisions with other code.
Finally, if you really, really, really wanted to change every single #super-product-list select.qty element on the whole page to '10', you would do this in Prototype.js:
$$('#super-product-list select.qty').invoke('setValue', 10);
Related
I want to add a filtered listener to the change event of a form's children but I am getting weird results from the $ selector.
I call the selector with the id of the form $("exportForm") and try to call the .on(...) method on it, getting the error in question.
Inspecting the returned element I seem to find an array with numbers as ownProperties names. Indexing them in the console $(...)[1] returns the single children of the form. In the proto property there seem to be no trace of Prototype.js methods which should be added by the selector.
What is going wrong? What to look for to get it working?
PS: Prototype.js version is 1.6.1
You're right. on() was added in 1.7, so you can't use it here. What the on method gives you is "lazy evaluation". When you call
$(document).on('eventName', 'css selector', function(){ ... });
...you get an observer that doesn't have to be initialized after the page loads, or after parts of the page are replaced by Ajax callbacks. You can replace a part of the page, then click on it again, and the observer will just work.
The old way to set up an observer was this:
$(document).observe('dom:loaded', function(){
$('theId').observe('eventName', function(){
// do something
});
});
The outer observer waits until the page is finished loading, then the inner one observes some object for an event and does something with that. This will work, as long as the DOM doesn't change after it loads. If that happens, then you have to re-register the inner listener manually, since its targets have changed, and it no longer will work. The dom:loaded event only fires once, when the page itself is loaded the first time.
In your case, it seems that you want to duplicate the behavior of the Form.Element.Observer() class method in Prototype, which sets up a timed poll of all a form's child elements, and fires a callback when any of them change. You really ought to look at the documentation for that method, as it is a really bulletproof way to do what you're trying to do. But in case you really want to roll your own, here's how you would write an observer that could listen for events on multiple elements:
$(document).observe('dom:loaded', function(){
$$('#exportForm input').invoke('observe', 'change', function(evt, elm){
// evt is the event itself, you can cancel, log, whatever
// elm is a reference to the form element, you can do elm.getValue(), etc.
});
});
This uses the "double-dollar" method to get an array of elements, so any form input inside the form with the ID exportForm is captured.
invoke() applies the same callback and arguments to an array of elements, so this is setting up a separate observe method for each form element -- not efficient if you have lots of inputs! You can try to listen for the change event on the form itself, so you only have one observer method, but depending on the browsers you need to support, you may find that these events don't always bubble from the individual form input up to the form itself. You'll have to test carefully.
Each form element observed will be seen in isolation: you won't have access to the rest of the form inputs from within the callback, just that one.
That's why the Form.Element.Observer is so powerful -- it gives you a hash of the entire form each time it fires, so you can do things like create previews or validations on a frequent-enough basis to appear "live".
I am trying to replace confirm() boxes with bootstrap modals for save/delete operations in a page.There is a button(call it action-button) for each of these operation which then triggers the respective modal. On clicking the confirm button on the modal,the respective function is triggered.
Instead of writing modals for each of these operations,I was thinking if I could use just one confirm modal and change the event handler when the "action-button" is clicked.
Like this:
$("#confirm-button").attr('onclick',save_all());
when save_all button is clicked.
Is it bad practice?And what are some alternatives to this? Thanks!
I don't think changing the event handler is a good choice as you should handle the previous attached handlers in order to avoids memory leak and weird behaviours.
A better approach could be defining a generic handler that will read from a data attribute on the DOM, what to do in case of confirmation.
The main part is that such handler should not been coupled with the working staff.
To achieve this you could define custom events that will be fired from your generic handler.
I mean something like:
function confirmAction(ev) {
// you can save in the HTML the name of the proper event
var whatEv = $(this).data('my-key');
// Here you could fire a custom event.
$(this).trigger(whatEv);
}
Last thing, as #trincot pointed, your code in the OP is wrong.
It will not even do what you intend, because you call save_all at the moment you set the attribute, and it will be the function's return value that is stored in the onclick attribute, converted to string if it is not yet a string. This most probably is not what you intended to do.
Even if the attribute value would be correct, it is not the best practice, because:
That attribute value needs to be evaluated/parsed, which means you can get late syntax errors
It replaces the previous value of the onclick attribute, which might be an undesired effect (although I understand in your case that is what you want)
Instead bind event handlers the jQuery way and make sure you don't call the function, but pass its reference:
$("#confirm-button").on('click', save_all);
If the purpose is to replace any previous click handler, then chain in an off call:
$("#confirm-button").off('click').on('click', save_all);
I am loading an ajax form with inputs that I apply .selectize() to. I run into issues when I close and reopen the form because there are instances of the Selectize constructor function that are still around.
Is there a way to remove these instances when I close the form? I can see these objects build up as I open and close the form by looking through firebug in the DOM under Selectize.count. How do I access these instances and destroy them?
I have tried this:
instance1[0].selectize.destroy();
instance2[0].selectize.destroy();
Assigned the variables like this:
instance1 = $('#preferences_sport').selectize({
//custom code
});
instance2 = $('#preferences_sport').selectize({
//custom code
});
The Selectize.count continues to build up and I am not sure where to go from here.
Here is a JSFiddle where I show the objects building up
So I see what you are saying now that the fiddle was added. I began by searching the documentation for that count property. I couldn't find it. So next I searched the source code since it seems this is some undocumented thing. The only count I could find in source code was this line:
eventNS : '.selectize' + (++Selectize.count),
So basically this explains it. That count while it does increase for every element this is called on is not a current count of running widgets. Its an internal property the guy who wrote this uses as a GUID for event namespaces. So for instance when you call destroy he can only remove the events specific to that instance of the widget.
I would not use this property to tell you anything. I think its safe to assume that your destroy is working fine. If you are unfamiliar with event namespacing you can read more about it here:
https://api.jquery.com/event.namespace/
You can see he uses that eventNS throughout the code to attach events if you search for it. jQuery does this in their code too for lots of stuff like in their event and data code. They have a GUID variable they use so anyone who loads more than one instance of jQuery on a page, the instances won't step on each others toes.
So I guess the only thing I would now ask you is where did you learn about this count property? If you just found it and assumed that it meant this was a count of running instances try to remember to always check with the documentation. If you found it in the docs then please point me towards now so I can take a look and see if it verifies what I found or requires more looking into.
Also as a bonus heads up, I saw this in your fiddle, input is a self closing tag or also known as void elements.
<input type="text" value="Calgary, Edmonton" class="selectize_this"></input>
Should just be:
<input type="text" value="Calgary, Edmonton" class="selectize_this" />
From the spec:
Void elements can't have any contents (since there's no end tag, no
content can be put between the start tag and the end tag).
Void elements: area, base, br, col, embed, hr, img, input, keygen,
link, meta, param, source, track, wbr
The Selectize API does expose the following method:
destroy()
Destroys the control and unbinds event listeners so that it can be garbage collected.
There are a lot of questions about binding future manipulations to non-existent elements that all end up answered with live/delegate. I am wondering how to run an arbitrary callback (to add a class or trigger a plugin, for example) to all existing elements that match a selector and all future elements that match that same selector that are yet to be created.
It seems that the main functionality of the livequery plugin made it into the core but the other part, attaching arbitrary callbacks got lost along the way somehow.
Another common answer is event delegation but what if one doesn't have access to all of the vendor code that is creating the elements to have it trigger the events?
Here is some real-world code:
// with livequery
$('input[type=text], input[type=password], textarea, .basic_form .block select, .order_form .form_item select, .order_form .form_item input')
.livequery(function(){
$(this)
.focus(function(){
$(this).addClass('active');
})
.blur(function(){
$(this).removeClass('active');
})
.addClass('text');
});
// with live
$('input[type=text], input[type=password], textarea, .basic_form .block select, .order_form .form_item select, .order_form .form_item input')
.live('focus', function(){
$(this).addClass('active');
})
.live('blur', function(){
$(this).removeClass('active');
});
// now how to add the class to future elements?
// (or apply another plugin or whatever arbitrary non-event thing)
One approach would be to monitor when new nodes are added/removed and re-trigger our selectors. Thanks to #arnorhs we know about the DOMNodeInserted event, which I would ignore the cross-browser problems in the hope that those small IE patches could someday land upstream to jQuery or knowing the jQuery DOM functions could be wrapped.
Even if we could ensure that the DOMNodeInserted fired cross-browser, however, it would be ridiculous to bind to it with multiple selectors. Hundreds of elements can be created at any time, and making potentially dozens of selector calls on each of those elements would crawl.
My best idea so far
Would it maybe be better to monitor DOMNodeInserted/Deleted and/or hook into jQuery's DOM manipulation routines to only set a flag that a "re-init" should happen? Then there could just be a timer that checks that flag every x seconds, only running all those selectors/callbacks when the DOM has actually changed.
That could still be really bad if you were adding/removing elements in great numbers at a fast rate (like with animation or ____). Having to re-parse the DOM once for each saved selector every x seconds could be too intense if x is low, and the interface would appear sluggish if x is high.
Any other novel solutions?
I will add a bounty when it lets me. I have added a bounty for the most novel solution!
Basically what I am getting at is a more aspect-oriented approach to manipulating the DOM. One that can allow that new elements are going to be created in the future, and they should be created with the initial document.ready modifications applied to them as well.
JS has been able to do so much magic lately that I'm hoping it will be obvious.
In my opinion, the DOM Level 3 events DOMNodeInsertedhelp (which fires only for nodes) and DOMSubtreeModifiedhelp (which fires for virtually any modification, like attribute changes) are your best shot to accomplish that task.
Of course, the big downside of those events is, that the Internet Explorers of this world don't support them
(...well, IE9 does).
The other reasonable solution for this problem, is to hook into any method Which can modify the DOM. But then we have to ask, what is our scope here?
Is it just enough to deal with DOM modification methods from a specific library like jQuery? What if for some reason another library is modifying the DOM or even a native method ?
If it's just for jQuery, we don't need .sub() at all. We could write hooks in the form of:
HTML
<div id="test">Test DIV</div>
JS
(function(_append, _appendTo, _after, _insertAfter, _before, _insertBefore) {
$.fn.append = function() {
this.trigger({
type: 'DOMChanged',
newnode: arguments[0]
});
return _append.apply(this, arguments);
};
$.fn.appendTo = function() {
this.trigger({
type: 'DOMChanged',
newnode: this
});
return _appendTo.apply(this, arguments);
};
$.fn.after = function() {
this.trigger({
type: 'DOMChanged',
newnode: arguments[0]
});
return _after.apply(this, arguments);
};
// and so forth
}($.fn.append, $.fn.appendTo, $.fn.after, $.fn.insertAfter, $.fn.before, $.fn.insertBefore));
$('#test').bind('DOMChanged', function(e) {
console.log('New node: ', e.newnode);
});
$('#test').after('<span>new span</span>');
$('#test').append('<p>new paragraph</p>');
$('<div>new div</div>').appendTo($('#test'));
A live example of the above code can be found here: http://www.jsfiddle.net/RRfTZ/1/
This of course requires a complete list of DOMmanip methods. I'm not sure if you can overwrite native methods like .appendChild() with this approach. .appendChild is located in Element.prototype.appendChild for instance, might be worth a try.
update
I tested overwriting Element.prototype.appendChild etc. in Chrome, Safari and Firefox (official latest release). Works in Chrome and Safari but not in Firefox!
There might be other ways to tackle the requirement. But I can't think of a single approach which is really satisfying, like counting / watching all descendents of a node (which would need an interval or timeouts, eeek).
Conclusion
A mixture of DOM Level 3 events where supported and hooked DOMmanip methods is probably the best you can do here.
I was reading up on the new release of jQuery, version 1.5 and I immediately thought of this question.
With jQuery 1.5 you can actually create your own version of jQuery by using something called jQuery.sub();
That way you can actually override the default .append(), insert(), .html(), .. functions in jQuery and create your own custom event called something like "mydomchange" - without it affecting all other scripts.
So you can do something like this (copied from the .sub() documentation with minor mod.):
var sub$ = jQuery.sub();
sub$.fn.insert = function() {
// New functionality: Trigger a domchange event
this.trigger("domchange");
// Be sure to call the original jQuery remove method
return jQuery.fn.insert.apply( this, arguments );
};
You would have to do this to all the dom manipulation methods...
jQuery.sub() in the jQuery documention:
http://api.jquery.com/jQuery.sub/
Great question
There seems to be a custom event you can bind:
http://javascript.gakaa.com/domnodeinserted-description.aspx
So I guess you could do something like:
$(document).bind('DOMNodeInserted',function(){ /* do stuff */ });
But I haven't tried so I don't have a clue..
btw.: related question:
Can javascript listen for "onDomChange" on every Dom elements?
There is no simple obvious way to do it. The only surefire approach is active polling, which causes there to be a render hiccup between when the new element is created and when the polling notices it. That can also make your page take a lot of resources depending on how frequently you poll the page. You can also couple this, as you observed, with binding several browser-specific events to at least make things work out better in those browsers.
You can override jQuery's DOM modification functions to trigger a custom change event (and use $.live to catch those events for manipulation), but when I've tried this in the past, it's subtly broken various jQuery plugins (my guess is some of those plugins do something similar). In the end I've given up on doing so reliably since I don't want to give up the performance and render hiccups to active polling, and there is no other comprehensive way to do it. Instead I have an initialization event I make sure to trigger for each DOM change I make, and I bind my manipulation events to those instead.
Be careful, it's easy to get stuck in an infinite event loop if you don't think things through, and this can also be subtle and difficult to track down; and worse yet may happen for a corner case your unit testing didn't allow for (so your users experience it instead of just you). The custom manually triggered initialization event is easier to diagnose in this sense since you always know exactly when you're triggering it.
Well, first of all, you shouldn't even be using selectors like this if you're worried about perf.
$('input[type=text], input[type=password], textarea, .basic_form .block select, .order_form .form_item select, .order_form .form_item input')
Any browser that doesn't have native implementations of xpath (more than just IE iirc) or getElementsByClassName (IE 7 and below) could easily spend a few seconds chewing on that on a big site so polling would of course be completely out of the question if you want it that broad.
I'm trying to implement a simple horizontal navigation menu that just shows a single div for each link. It is kinda like a dropdown menu but instead of a mouseover triggering a dropdown, an onclick event will trigger the showing of a div. I want to make sure I am taking the right approach before going too much further, any help is appreciated. This is what I have so far:
<ul id="settings_nav">
<li>
<a>Theme</a>
<div id="settings_block"><%= render :partial => 'email_password' %></div>
</li>
<li>
Lists
<div id="settings_block"><%= render :partial => 'lists' %></div>
</li>
</ul>
window.onload = function(){
settingsMenuInit('settings_nav')
}
function settingsMenuInit(settings_nav){
$(settings_nav).childElements().each(
function(node){
node.onclick= function(){ this.next.show() };
})
}
Something like that, but I am unsure how to get the div that is currently shown and hide it. I could iterate through all the childElements and hide each div and then show the one that is being clicked, but maybe there's a better way?
Some notes FW(T)W:
With Prototype and similar libraries, you don't want to hook up event handlers by assigning functions to the element's onclick and similar properties; that style has several disadvantages (not least that there can only be one handler for the event on the element). Instead, use Prototype's observe function:
someElement.observe('click', functionRefHere);
// or
Element.observe(someElementOrID, 'click', functionRefHere);
This also lets Prototype work around some IE memory loss bugs for you.
You might look at is Prototype's dom:loaded event, which happens sooner than window.onload (which won't happen until all of your images and other external resources have loaded, which can be a second or two after the page is displayed):
document.observe('dom:loaded', initFunctionRefHere);
You can use event delegation and just watch your settings_nav element, rather than each child node individually.
$(settings_nav).observe('click', handleNavClick);
function handleNavClick(event) {
var elm = event.findElement("some CSS selector here");
if (elm) {
event.stop();
// Handle it
}
}
As you can see, Event#findElement accepts a CSS selector. It starts with the actual element that was clicked and tries to match that with the selector; if it matches, it returns the element, otherwise it goes to the parent to see if it matches; etc. So with your HTML you might look for a li (event.findElement('li')) or the link (event.findElement('a')).
But if you want to watch each one individually, they can share a function (as they do in your example):
$(settings_nav).childElements().invoke('observe', 'click', handleNavClick);
function handleNavClick(event) {
// Prototype makes `this` reference the element being observed, so
// `this` will be the `li` element in this example.
}
Whether you watch each element individually or use event delegation depends on what you're doing (and personal preference). Whenever anything is likely to change (adding and removing navigation li elements, for instance) or when there are lots of things to watch, look to event delegation -- it's much easier simpler to deal with changing sets of elements using event delegation and just watching the parent. When dealing with a stable structure of just a few things (as in your example), it may be simpler to just watch the elements individually.
Once inside your handler, you can use Element#down to find child elements (so from the li, you might use li.down('div') to find the div), or Element#next to get to the next sibling element (e.g., going from the link to the div). Either way, once you have a reference to the div, you can use Element#show and Element#hide (or Element#toggle).
I recommend using named functions instead of anonymous ones (see my example above). Named functions help your tools (debuggers, browsers showing errors, etc.) help you. Just be sure not to declare a named function and use it as an expression (e.g., don't immediately assign it to something):
// Don't do this because of browser implementation bugs:
someElement.observe('click', function elementClickHandler(event) {
// ...
});
// Do this instead:
someElement.observe('click', elementClickHandler);
function elementClickHandler(event) {
// ...
}
...because although you should be able to do that according to the spec, in reality various bugs in various browsers make it not work reliably (article).