This patch introduces a jQuery .once()
method which streamlines the way behavior functions work. Currently, we have to manually ensure that an element is only initialized once. Usually, this happens by adding classes and selecting only those elements which do not have that class. However, this process can be separated out into a jQuery ‘filtering’ function which does all the grunt work.
This is the code we used before this patch:
$('select.timezone-detect:not(.timezone-processed)', context).addClass('timezone-processed').each(function() { // ... };
And this is the streamlined version:
$('select.timezone-detect').once('timezone', function() { // ... };
The .once()
function takes one or two parameters: if there’s just one and if it’s a string, it filters out all elements which already have the processing function with that label run. If there is a second parameter, it runs that function for each matched elements that has not yet been processed, similar to .each()
. All ways how this function can be used are documented in the source code.
Comment | File | Size | Author |
---|---|---|---|
#70 | drupal.jquery-once-58-add-once-only.patch | 3.25 KB | bowersox |
#63 | install_screen.png | 83.33 KB | rfay |
#59 | drupal.jquery-once-58.patch | 25.12 KB | sun |
#58 | drupal.jquery-once-57.patch | 19.02 KB | sun |
#54 | 444344.patch | 23.89 KB | RobLoach |
Comments
Comment #1
kkaefer CreditAttribution: kkaefer commentedComment #2
kkaefer CreditAttribution: kkaefer commentedComment #3
kkaefer CreditAttribution: kkaefer commentedFixed a small bug in .once()
Comment #4
caktux CreditAttribution: caktux commentedGetting no errors in IE6 and 7, FF and Safari. A big +1 from me for a commit, I haven't been through everything but its looks like a great cleanup that doesn't break anything and just improves the way jquery is used.
Comment #5
caktux CreditAttribution: caktux commentedI guess it was too big to notice at first, but I now get the Body textareas that disappear in all browsers when adding a node (tried blog and forum types).
Comment #6
webchickSounds like needs work then.
Comment #7
kkaefer CreditAttribution: kkaefer commentedAfter a long fight with teaser.js and textarea.js, the textareas are now back. I refactored some of the code that initialized the teaser splitter to reduce the dependencies and race conditions between those two script.
Comment #8
RobLoachThis patch is awesome. Adding some tags........... Somewhat related issue for behaviors that are more then once: #404902: Replace Drupal.behaviors with jQuery.live().
Comment #9
quicksketchThis is a great idea. It'll be nice to continue adding new native jQuery methods like the setSummary and getSummary methods added in the #323112: Vertical Tabs patch.
Despite being shorter and easier to implement, I think we're going to cause a large amount of confusion with this method. Upon looking at this issue, I immediately thought, "Oh I didn't know jQuery had a once() method!", and then went to docs.jquery.com to look it up. When it wasn't there I downloaded the source to jQuery itself and searched for "once". Then I was generally just confused and decided to read the patch. ;-)
The same thing happened when I looked at Vertical Tabs, but I figured that one out much faster because I was already pretty sure that set/getSummary weren't native jQuery methods. However, the shortness of this method name, and it's generic utility, make it seem to be a very likely method to be provided by jQuery itself, and for people looking for documentation on how to use
once()
, they'll probably experience the exact thing I went through, searching desperately for where this method comes from and how to use it.So, I'd like to ask, is there some way we can name-space these methods as being Drupal-specific? While I agree that "once" is the perfect name for this method, it's completely unclear to me that Drupal would add this method. Considering all the jQuery methods are just thrown into a "function soup" after they've been defined, we're risking name-space collisions with other libraries that might declare the same methods. This is a bit different from PHP namespace problems, which for the most part we just assume people don't combine Drupal with other PHP programs in the same memory, but for jQuery methods, it's completely common for a single site to be using many different jQuery libraries that all need to share the same space.
Some possibilities to avoid this problem. Prefix all methods with "drupal" or "d", like drupalOnce and drupalSetSummary. Or make a universal class for Drupal methods, so we could use $().drupal.once() and $().drupal.setSummary().
Comment #10
quicksketchIt looks like adding an object within jQuery is not as common as directly declaring methods, but it's not unheard of. Some projects that namespace multiple methods within a single object:
http://plugins.jquery.com/project/AjaxFilter
jQuery.ajaxFilter.register()
http://plugins.jquery.com/project/zendjsonrpc
jQuery.Zend.jsonrpc({url: 'rpc.php'});
If we start converting a lot of Drupal's JavaScript to be jQuery methods, we'll run into name spacing issues frequently. Some method names that are already "claimed" by other jQuery libraries that we'd probably want to use:
thobber()
progressbar()
autocomplete()
There are also jQuery plugins for just about everything Drupal core offers (table drag and drop, resizable textareas, sticky table headers, etc.) Note that I'm not suggesting we should use the jQuery plugins instead of using our own, I'm saying that it'd be great to convert Drupal's core JS files to be jQuery methods, but not collide with other plugins from the broader jQuery community.
Comment #11
RobLoachRegarding Nate's comments about deciding on a convention for these jQuery extensions that we create explicitly for Drupal, I think he's right in saying that we should namespace it, but at the same time, I think that it should be tackled in a separate issue.
We had a brief discussion about it and thought that namespacing it into a "Drupal" class might be a good idea:
$(".something").Drupal.once();
$('#field-summary').Drupal.setSummary();
Using the uppercase for the JavaScript class name seems like the way to go as Sizzle.js uses it, as does Processing.js.
In hopes to not de-rail this thread, I created #445130: Namespace the Drupal jQuery functions. Other then that, I'm very confident with this patch.
Comment #12
caktux CreditAttribution: caktux commentedPerfect in all browsers here, I think for this issue it just needs review, this cleanup would not interfere with future namespacing. Just to make sure, any theming hooks using those .addClass('ahah-processed') that get removed?
Comment #13
kkaefer CreditAttribution: kkaefer commentedUnfortunately, it’s not possible to use something like
$(...).Drupal.once()
due to how JavaScript handlesthis
in object hierarchies. Using$.Drupal.once()
isn’t quite that straightforward either, since we’d have to use:instead of
Nate suggested to prefix all custom-defined funtion names with
drupal
, which is an OK solution:or
I wonder, though, whether we could use another prefix or suffix, for example
$
or_
:or
Comment #14
RobLoachMy vote is in for
$('select.timezone-detect').drupalOnce();
. The other presented solutions just don't really look that pretty.Although I'd rather not use it, another solution Konstantin missed here would be to take
$d
as a Drupal JavaScript function, like how jQuery $ is a JavaScript function.........$d('select.timezone-detect').once()
. Again, my vote is still in for$('select.timezone-detect').drupalOnce();
.Comment #15
kkaefer CreditAttribution: kkaefer commented$d doesn't allow us to chain "Drupal-jQuery" and actual jQuery functions.
Comment #17
caktux CreditAttribution: caktux commentedWe need chaining...
Comment #18
kkaefer CreditAttribution: kkaefer commentedAs an alternative to the
$(...).once()
method proposed here, we can use the jQuery plugin available from http://plugins.jquery.com/project/once ;)Comment #19
kkaefer CreditAttribution: kkaefer commentedRerolled for most recent HEAD.
Comment #20
kkaefer CreditAttribution: kkaefer commented(Note that the copyright notice is only in there because I just copied the jQuery plugin's source code in there. We can remove it, if we want.)
Comment #21
mfer CreditAttribution: mfer commentedIf the .once() plugin is being enclosed in the wrapper
why is it enclosed by an anonymous function?
Comment #22
kkaefer CreditAttribution: kkaefer commentedTo "Wrap this into another function scope so that cache and uuid are private" (see the code comment before the function)
Comment #23
mfer CreditAttribution: mfer commentedUm, yeah. In the future I'll try to read those code comments that clearly state what's going on :)
Comment #24
RobLoachWent through all the JavaScript and tested the instances that used the new .once plugin. Worked nicely, kkaefer did a very thorough job here.
Comment #25
RobLoachInstead of sticking this in Drupal.js, what about sticking it in jquery.once.js, and having it always loaded by default? Like how jquery.js and drupal.js are loaded by default? This could also mean that we could minify it and just document that it's the jQuery Once plugin with the URL.
Comment #26
mfer CreditAttribution: mfer commented@Rob Loach I like the idea of having it separated from drupal.js. Since this is technically an external library. We should have it separated out for easier updates, swapping it out with modules like jQuery Update when needed, and maintainability in general.
Marking CNW to do this change.
Comment #27
RobLoachMoved to jquery.once.js.
Comment #28
caktux CreditAttribution: caktux commentedThis is a great idea. I wrote a big paragraph about it all a few seconds ago and lost it, dang! try again (I now have the Lazarus plugin in Firefox...)
So, I was going to say that this could be a great way to solve the problem of adding jQuery UI components to core #315035: Add jQuery UI elements to core. We could add a stable ui.core.js (or stick with a stable, or even custom one?) for basic UI methods. We could also have a custom (or stable version of) ui.draggable.js and ui.dialog.js for example. It would be a great way to have better jQuery support, provided we document it well of course. Personally, when coding a jQuery plugin or anything with jQuery, I don't really care about the methods I'll have to use. First, because it's jQuery anyway, and second, I just want some UI methods available so I don't have to reinvent the wheel, and mostly know that everything will be consistent in my module's JavaScript. The #315100: Allow to add JS/CSS libraries (sets of files, settings, and dependent libraries) could still exist for extra UI plugins since that needs to be addressed also. I still think this should be left for modules to handle and trying to concentrate efforts on that a little more for plugins.
There's also that my imageplacement module depends on ui.core and ui.draggable, and that with imagefield in core, it's something that I'd like to see there too ;)
Comment #30
RobLoachUpdated to HEAD.
Comment #32
RobLoachUpdated to HEAD.
Comment #34
lilou CreditAttribution: lilou commentedSetting to 'needs review' - testbot was broken.
Comment #36
RobLoachUpdated with use of #315100: Allow to add JS/CSS libraries (sets of files, settings, and dependent libraries) as well as updated the Toolbar to use it.
Comment #38
RobLoachUpdated to HEAD.
Comment #39
RobLoachQuicksketch found a couple of places where the context wasn't brought over. tha_sun asked for a -N cvs reroll.
Comment #40
sunI'm not sure whether this .once() approach is the way to go... it equals more or less what we're doing all over the place currently, but instead of introducing a (slow) class-based lookup, we might have a look into jQuey.once() and advance on that instead...? (I didn't look at that yet, but in general, the _direction_ feels wrong)
Comment #41
sunAlso, I think that this patch doesn't account for detaching behaviors. I can understand that detaching behaviors are hard to support, given that Drupal core doesn't require/use them on its own... but still.
If I get this right, .once() should be named .attach() - to match our current naming...?
well, it shouldn't be called .attach(), because attaching behaviors are invoked multiple times. But given that confusion, there should probably also be an .attach() behavior.
Comment #42
RobLoachAre you thinking of jQuery live() and die()? The unfortunate thing about them is that they only supports a set of events, and don't give "setup" or "cleanup" events. There was a short discussion about this over at #404902: Replace Drupal.behaviors with jQuery.live(), and I think it would be great, but it doesn't give us all the support we need yet. Konstantin called the plugin jQuery Once, and that does make sense.
You're right about detaching behaviors. What we could look at is extending once to support removeOnce(), which would check for -processed, remove the class and run the actions. Any thoughts? Know of any place in core that removes the behaviors yet?
Regarding the performance of class manipulation, litwol brought up the idea of using jQuery Data to store the information, but that might be out of scope for this issue....
Comment #43
sunheh, yes, seems like I was horribly confused. That should have read "but instead of introducing a (slow) class-based lookup, we might have a look into jQuey.live() and advance on that instead", i.e. take over the internal mechanism to attach events only once to certain elements.
As of now, I think that we do not have any .detach() method in core. I pushed that patch in for better wysiwyg support in D7. I'm not sure which other use-cases exist for detaching behaviors, but it's possible there are some.
Since we already define behaviors with .attach() and .detach() methods, maybe it would be most sane to name the functions here $.attachOnce() and $.detachOnce() ? OTOH, I'd say that behaviors are always detached only once.
Another issue with $.once() is that detach() methods need to know/access the storage to determine whether they need to detach. So if we abstract the attaching, we should also abstract the detaching.
Comment #45
deekayen CreditAttribution: deekayen commentedbad trial on new testing bot
Comment #47
deekayen CreditAttribution: deekayen commentedbad trial run of new testing bot again
Comment #48
kkaefer CreditAttribution: kkaefer commentedThe reason we're not using .data() for this is that you might want to apply stylesheets when the element is processed.
Comment #50
RobLoachThis includes
$.removeOnce()
which acts on elements that have already been processed by once() and removes the processed class. It's not used anywhere in core yet since we don't detach any elements. Almost seems out of scope for this issue, but its reasons are valid. Thanks, sun.This patch also doesn't touch ajax.js since it seems there are still a bunch of patches going into it at #544418: Integrate CTools AJAX framework with Drupal to extend (and replace) existing ahah framework. We can use jQuery Once on it as soon as the patches on it slow down.
Comment #51
sunIn general, this looks nice and makes sense.
Though I'm still not sure whether we should not name these methods .attachOnce() and .detachOnce(), following our current naming pattern.
Along with the following remarks, I'd propose to move this into drupal.js, since we will do various adjustments anyway.
That said, it seems like most of our attaching behaviors just contain $(selector).once(class, function); with that now. I wonder whether it wouldn't make sense to enhance our behavior object with attachOnce and detachOnce methods, so Drupal.attachBehaviors() would take over the handling of $.once().
"Filters elements by whether they have not yet been processed."
s/it will be used in the class name applied/then it will be used as a CSS class name that is applied/
The class name should be wrapped in single quotes.
I don't understand this part. There seem to a variety of ways to invoke $.once(), so ideally we explain them in the JSDoc, potentially using example code snippets.
No, the function follows $.each()'s logic:
return false; entirely breaks the iteration.
return;/return true; continues to the next matched element in the set.
If we move this drupal.js, may we remove those? In case not, that stuff belongs to a starting @file JSDoc at the top of the file, not into every single function.
What happens if id is an array or object?
Additionally, the comment should be reworded to not claim that it validates the id for usage in a CSS class, because that does not happen.
It seems this cache does not take context into account?
I believe a "no" is missing here. Could also be "a". But then again, that comment does not make much sense when looking at the code that follows.
Can we write this on two separate var lines, please?
s/already//
Quite possibly, I can't retrieve the auto-generated uuid in the cache, when $.once() was used without id? Can we make the internal cache accessible?
12 days to code freeze. Better review yourself.
Comment #52
RobLoachBack when this issue began, we realized that others might like to take advantage of
$().once()
, so Konstantin stuck up http://plugins.jquery.com/project/once . I think it's best not to fork off of it, but rather keep it at the upstream. Managing it as jquery.once.js rather then sticking it as part of drupal.js also allows us to manage it through hook_library() so that we could swap out an updated version of it later on through contrib if we wanted to. It is technically an external library. When JavaScript aggregation is on, it will be merged into one file anyway.This patch fixes a bunch of the documentation issues that sun brought up. Looks pretty good now. For the JSDoc file header, I copied what jquery.form.js has.....
Comment #53
sunPlease write these on separate lines.
I think we should live this as 'tableheader' (all-lowercase) to match the name of the behavior.
Same here. Though, not sure whether we couldn't strip "selector".
Let's convert .size() into .length while being there.
This hunk is actually the most critical in the patch - I'm not sure whether the resulting behavior is still the same. (I really hope we split this code into a separate install.js.)
I don't think it makes sense to list all required libraries here. Just state that we add the required. ;)
Please use .length here, too.
Can we squeeze a blank line in between there?
Aside from those issues, I think this is ready to fly.
I'm on crack. Are you, too?
Comment #54
RobLoachAddresses all points in #53 by sun.
Comment #55
kkaefer CreditAttribution: kkaefer commentedThe name of the behavior is
tableHeader
, nottableheader
.Not sure why
.length
should be better. Either way, it’s an academic discussion and I don’t care.I applied the patch and tried a various JavaScript functionalities and I couldn’t discover any apparent malfunctions.
Comment #56
sun@kkaefer: $.size() is a dumb wrapper around {}.length. {}.length thereby avoids a function call, but is also more comprehensible to people who understand basic JavaScript, but not yet all of jQuery. I actually hope that jQuery folks will remove .size() to remove this confusion.
Following the names that have been applied to core behaviors, I think this should state:
This review is powered by Dreditor.
Comment #57
webchickLooks like this still needs work.
Comment #58
sunIncorporated those changes.
Comment #59
sunNot sure why, but some hunks got lost in my previous patch.
Comment #60
pwolanin CreditAttribution: pwolanin commentedtagging
Comment #61
Dries CreditAttribution: Dries commentedGreat clean-up. Committed to CVS HEAD. Thanks.
Comment #62
RobLoachExcellent! Thank you so much, Dries. Marking as Needs Documentation so we remember to add the upgrade docs.
Comment #63
rfayIt looks to me like this broke install.php. I found myself unable to do an install.php this morning and worked through yesterday's commits. This commit was the one where install.php ceases to work.
Attached is the screenshot of what happens to install.php. It just stops.
Comment #64
dmitrig01 CreditAttribution: dmitrig01 commentedInstalling head with jQuery is broken.
Comment #65
clemens.tolboomOn a sqlite D7 (after applied #5 from #476010: Drupal 7+SQLite fails to install I get this as content for
misc/jquery.once.js
Comment #66
clemens.tolboomHmmm ... jquery.once.js is not in HEAD
reverting and then applying the patch installs drupal
Comment #67
clemens.tolboomPlease (re)apply the patch to HEAD
Comment #68
yched CreditAttribution: yched commentedIs it only me or vertical tabs quit working ?
Comment #69
roychri CreditAttribution: roychri commented@yched - A lot of different Javascript stuff is broken with the latest commit from this issue.
As clements.tolboom mentionned, you can simply revert the patch in #59 and then reapply it and all should be fine. It worked for me.
It appear that the file misc/jquery.once.js was not added/committed to CVS even though the patch is creating it.
The only difference you should see after running these two commands is the creation of the file misc/jquery.once.js
Comment #70
bowersox CreditAttribution: bowersox commentedI've attached a patch that only adds in the missing jquery.once.js file, which may be cleaner than re-applying the entire patch #59.
Comment #71
webchickI committed this. thanks!
Comment #72
RobLoachWe forgot the upgrade documentations, so I added it: http://drupal.org/update/modules/6/7#jquery_once
Comment #75
qqboy CreditAttribution: qqboy commentedplease kindly note that in d7
$(xxx).once(function(){})
this usage seems not compatible in D8, I used $(this).once().click(function(){}) works.