Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem:
- Certain JavaScript behaviors cannot detach from dynamic contents before contents are removed.
Goal:
- Like
Drupal.behaviors
, add a stack of functions that do the opposite, i.e.Drupal.unbehaviors
orDrupal.debehaviors
(sorry, language barriers).
Details:
- The issue arose while implementing Wysiwyg API support for Panels. TinyMCE, for example, implements a stack of editors that have been added to the page. Panels uses ajaxified modal dialogs to load forms for configuring a "pane" (similar to a block in Drupal core).
Drupal.behaviors
allows Wysiwyg API to attach an editor to textareas in the dynamically loaded Panels pane form. However, it has no chance to detach and remove the editor instance from TinyMCE's stack when the form (in a modal dialog) is submitted or maybe even dismissed (by closing the modal without submitting the form).- To allow modules to perform actions before dynamic content is unloaded from a page, we need an array of functions that need to be executed in front of removing a content, i.e. "detaching behaviors".
Example:
- Panels invokes
Drupal.attachBehaviors();
after loading a pane configuration form in a modal dialog
and invokesDrupal.detachBehaviors();
in front of submitting it via AJAX, or, in front of executing the click handler that closes the modal dialog without submitting the form.
- Wysiwyg API's simplified implementation:
/** * Save contents back into textareas and remove editor instances. */ Drupal.debehaviors.wysiwygDetach = function(context) { $('.wysiwyg-processed', context).each(function() { // Invoke Wysiwyg API function to save contents... // Invoke Wysiwyg API function to remove editor instance... }); }
Attached patch implements a Drupal.debehaviors stack along with the Drupal.detachBehaviors() function, which modules can invoke before removing/unloading dynamic contents.
Comment | File | Size | Author |
---|---|---|---|
#17 | drupal.detachBehaviors.patch | 88.96 KB | sun |
#15 | custom.tgz.txt | 477 bytes | sun |
#14 | drupal.detachBehaviors-1-compat.patch | 2.45 KB | sun |
#14 | drupal.detachBehaviors-2-indent.patch | 88.96 KB | sun |
#14 | drupal.detachBehaviors-3-less-changes.patch | 17.86 KB | sun |
Comments
Comment #1
sunBetter title?
Comment #2
nedjoI can see the need, and the idea of a detachBehaviour method seems good.
It seems awkward to have to register separate attach and detach callbacks.
Maybe we shd have both in the same function? Like:
Comment #3
dmitrig01 CreditAttribution: dmitrig01 commentedI personally find nedjo's proposal much more awkward. I don't like the names unbehavior or debehavior, but i like sun's approach otherwise.
Comment #4
nedjoI'm thinking now a behavior should be an object with attach and detach methods:
Comment #5
sunThought about this. The benefit from having separate callback functions for attaching/detaching are:
1) Performance: Not every attached behavior needs to be detached. So we can limit the invocations to functions that really need to detach.
2) Logic: Besides not having a big, nasty switch/if statement in each and every behavior, the actual logic of detaching behaviors is probably almost completely different to the attaching behavior in most cases (besides the CSS class name). Wysiwyg API is probably the only existing example currently; if you look at the attaching behavior in Drupal.wysiwyg.attach.tinymce(), I wouldn't really want to add another level of complexity to this function.
Comment #6
nedjo@sun: Evidently we cross-posted. Does the suggestion in #4 address your two concerns?
Comment #7
sun#4 looks much cleaner, but then again, I'd ask where's the difference between
and
I didn't think of this variant in the first place, since I wanted to keep API changes minimal. So, if Drupal.(de|un|mis)behaviors or similar won't work out at all (which would not require changes in all modules), then the last variant would be my favorite, because we could simply iterate over all attaching and detaching behaviors separately (and fast). We should bear in mind that probably only a small minority of all behaviors needs to detach from dynamically loaded contents.
Comment #8
dmitrig01 CreditAttribution: dmitrig01 commentedhow about
if there is an attach and detach, and if there isn't, then just
drupal.behaviors.myThingy
.use $.isFunction.
Comment #9
smk-ka CreditAttribution: smk-ka commentedI like Dimitri's idea, also because from an OO point of view,
Drupal.behaviors.myThingy.verb
seems a lot more logical.Comment #10
sunOh nice. Indeed, that's probably the most clean way to make detaching methods completely optional.
Tested attached patch with a custom module and it works as expected:
...is invoked just like before, and
is also invoked properly. :)
Comment #11
nedjoWell, my feeling is, if we're changing the form of this object, we should change it. Cleaner and clearer to have one form of an object than two different ones. We don't generally support backward compatibility. This is a simple change that won't take any time to upgrade. Let's drop the old form.
The patch has some stray tab characters instead of double spaces for indents.
Comment #12
sunI'm not sure whether I agree with that. Sure, it's more consistent - but turning all behaviors unnecessarily into objects with just one method can also be considered as cruft.
Anyway, leaving this decision to core maintainers.
Attached patch changes all behaviors throughout core.
Can someone set this to RTBC, please?
Comment #13
quicksketchSeems like we could save a lot of code changing here by just changing:
to
Of course either way works. But this makes the change much less intimidating and possibly more readable. I think this change in general is a great improvement.
Comment #14
sunOK. Hopefully, last re-roll now.
Attached are the 3 variants of this issue.
1) Backwards compatible API change, optionally introducing 'attach' and 'detach' methods.
2) API change, introducing 'attach' and 'detach' methods, by moving function contents of all existing behaviors into the 'attach' method.
3) API change, introducing 'attach' and 'detach' methods, by altering function/method declarations only (same as 2, but different syntax leading to less changes).
Comment #15
sunTo test Drupal.detachBehaviors(), here's a simple test module, which invokes the function upon clicking on any link.
Comment #16
Dries CreditAttribution: Dries commentedI reviewed the 3 possible solutions, and personally I think solution 2 of comment #14 (i.e. drupal.detachBehaviors-2-indent.patch) is the best solution from a design point of view. It also means that there is only one way to do things, which ultimately will pay off and make for more intuitive code and less frustration.
Comment #17
sunPatch still applies cleanly and works like a charm. This issue is on the list of killer feature enablers a.k.a. better WYSIWYG support: http://groups.drupal.org/node/6492/summary
I'd set it to RTBC if it wasn't my own patch. Already tested and triple-checked a few times now. I really cannot see any reason for holding this off.
Comment #18
Dries CreditAttribution: Dries commentedI've committed this to CVS HEAD. I'm marking this 'code needs work'; mark 'fixed' when the migration documentation in the handbook has been updated. Thanks sun.
Comment #19
sunAdded section to module upgrade docs: http://drupal.org/node/224333#drupal-behaviors
Comment #20
quicksketchThanks sun! Great work!
Comment #21
Anonymous (not verified) CreditAttribution: Anonymous commentedAutomatically closed -- issue fixed for two weeks with no activity.
Comment #22
sunSorry, but I have to re-open this issue and bump this to critical for 2 reasons:
1) Many JS gurus in here.
2) The problem:
As of now, Drupal core does not implement detaching behaviors yet, so we actually have no real-world code to test Drupal.detachBehaviors(), neither in core, nor in contrib. As some of you might have noticed, I heavily worked on Dreditor recently, which implements a pure, jQuery-based JavaScript application that only works on drupal.org. It is re-using Drupal's JavaScript environment on drupal.org, and because of that, many architectural concepts of Drupal have been adapted. Dreditor builds new content on the fly, but also removes and thereby tries to properly destruct content on the page. It is not yet using detaching behaviors, but while working on it, I found a major issue with the current design of detaching behaviors we added here:
We are always passing a regular DOM element as context. Detaching behaviors are supposed to detach from all elements in the context.
However, you cannot tell: Detach only from elements 24 + 25 + 32 + 48 (replace those numbers with arbitrary DOM elements), because currently, the passed in "context" has to be a surrounding container, i.e. a container holding many, arbitrary elements. That means, each detaching behavior will run on all elements within the context. There is no way to tell that we only want to detach from X and/or Y only, because all a detaching behavior gets to know is the (global) context.
Admittedly, this problem only applies to edge-case situations and not the common situation, in which you have a "context" that contains a certain DOM element tree in the current document only (regular AHAH/AJAX interface actions that add and remove contextual content).
The problem arises when a behavior acts on multiple slices of a context.
Let's consider the following markup:
Drupal.attachBehaviors(context) is invoked, where context == document:
Now, some nifty JavaScript wants to do
$('.menu li:first, .menu li:last').remove()
to replace "one" and "three" with something completely different. It thereby should invoke Drupal.detachBehaviors(). But since we only havecontext == document
, or possiblycontext == ul.menu
, our detaching behavior runs:The result: Our behavior is detached from all list items in the menu, although only the first list item is about to be removed.
The solution to this issue sounds quite simple, but is yet another major change to Drupal's behaviors: Always pass a jQuery object (which can contain certain elements only) as context (instead of one, plain, surrounding DOM element).
Apparently, for jQuery itself it is irrelevant whether context is a DOM element or a jQuery object.
And actually, a similar thread on the development mailing list discussed what "context" should be. To my knowledge, some contrib modules already get it wrong (passing a jQuery object instead of a DOM element).
However, there are two issues with a dirty "resolution" that just assumes anything:
a) The proper variable name for a context that is a jQuery object is
$context
.b) If behaviors accept a $context instead of a context, then they can safely assume that the following code will work:
Note that this may only apply to detaching behaviors, because attaching behaviors can simply check for the class "behavior-processed" to decide whether they need to run. Detaching behaviors can't rely on that.
To summarize: What we added here (detaching behaviors) is nice and badly needed. However, we have a fundamental design flaw in the function signature of attaching/detaching behaviors.
Comment #23
quicksketchI'm not sure this is necessary, since we can already pass in an array of DOM elements if we desire, and it will work with behaviors just like a single DOM element:
Both approaches work equally and work in every case I can think of. I don't think there's any need to change the API, since jQuery can accept an array of DOM elements completely interchangeably with a single DOM element.
Comment #24
sunok, that actually means we are passing a jQuery 'context', which is not a jQuery object, but either a DOM element, an array of DOM elements, or a jQuery object, which are all properly handled by jQuery internally.
To detach from certain elements only, those elements need to be either passed in a jQuery object or a DOM element array (i.e. aforementioned jQuery context).
That works.
We could clarify this in the documentation, but I'm not sure whether that belongs into this issue. Hence, closing.