Further to an irc discussion with sun, where I mentioned my original misunderstanding of how the use-ajax link class should work, i.e. to make a rendered link that can have an '#ajax' property, here's a re-roll of the patch I posted in that original issue #610068: Document AJAX no-js and use-ajax. It sounds like he has some bigger plans for ajax_process_form though anyway, as per #645822: #ajax is not extensible (and partially buggy).

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sun’s picture

Title: Make rendered links ajaxifiable » Make #type 'link' ajaxifiable

Thanks!

So this mainly means:

1) add #type = 'link' to the existing switch in ajax_process_form(), assigning 'event' => 'click' for anchors.

2) make ajax_process_form() form-$element-aware, i.e. before trying to use/access/assign stuff like $element['#name'], check whether we're in a form context.

3) invoke (a renamed) ajax_process_form() from within drupal_render(), just like we do for drupal_process_states() and drupal_process_attached().

casey’s picture

Any progression here? we don't have much time anymore.

fago’s picture

I've opened a related issue for using #ajax on a link from inside a form: #753270: Impossible to trigger form submit with a link
With adding "ajax_process_form" it's working pretty well - except for the little js fix needed (see patch over there). However #1 sounds like a good way to go for this one.

katbailey’s picture

Ok, I'll try and have this re-rolled as per #1 by tomorrow.

RobLoach’s picture

FileSize
5.52 KB

I'm trying to get this working on the "More information on filter tips" link on the node add form, but am having absolutely zero success.

katbailey’s picture

FileSize
14.77 KB
10.95 KB

OK well here's what I've got so far. You can test it with the attached version of the ajax_example module (the ajax link example) but I think we need more than this, just wanted to get this much up as a start...

sun’s picture

Status: Needs work » Needs review
Issue tags: +Ajax
FileSize
10.05 KB

Cleaned this up a bit.

sun’s picture

FileSize
11.09 KB

More. drupal_pre_render_link() didn't take #attributes into account. Only #options[attributes], WTF?

sun’s picture

FileSize
11.16 KB

Fixed missing url() for $element['#href'] in ajax_process_element(). #href is an internal system path.

Works pretty fine for me already. What's left?

sun’s picture

+++ includes/ajax.inc	24 Apr 2010 20:35:10 -0000
@@ -410,9 +410,9 @@ function ajax_footer() {
+function ajax_process_element($element, &$form_state = NULL) {

Should default to array()

+++ includes/ajax.inc	24 Apr 2010 20:35:10 -0000
@@ -443,6 +443,18 @@ function ajax_process_form($element, &$f
+      case 'link':
+        $element['#ajax']['event'] = 'click';
+        if (!isset($element['#attributes']['id'])) {
+          if (isset($element['#id'])) {

meh, should also take into account #options['attributes']['id']...

+++ includes/ajax.inc	24 Apr 2010 20:35:10 -0000
@@ -464,37 +475,43 @@ function ajax_process_form($element, &$f
+      $form_state['cache'] = TRUE;

Needs to be moved outside of this condition and set if the passed $form_state is !empty()

+++ includes/common.inc	24 Apr 2010 20:23:11 -0000
@@ -4716,6 +4716,10 @@ function drupal_pre_render_conditional_c
 function drupal_pre_render_link($elements) {
   $options = isset($elements['#options']) ? $elements['#options'] : array();
+  if (isset($elements['#attributes'])) {
+    $options += array('attributes' => array());
+    $options['attributes'] += $elements['#attributes'];

Consider to set effective attributes to $elements['#attributes']

93 critical left. Go review some!

katbailey’s picture

The way it works now, you'd have to have a #ajax array in your link element just so that it would be ajax processed although there wouldn't necessarily be anything you'd need to put in it, seeing as 'event' is already taken care of (defaults to click), and putting your 'wrapper' in there won't actually achieve anything. However it would make sense if you could put your wrapper in there and have that passed to your callback - this seems more sensible than having the menu callback decide where to append the content. So, something along the lines of (line 110 of ajax.js):

this.url = element_settings.url.replace(/\/nojs(\/|$)/g, '/ajax$1/' + this.wrapper);

So then your callback function would take two paramenters, e.g.

function ajax_link_response($type = 'ajax', $wrapper = '') {
  if ($type == 'ajax') {
    $output = t("This is some content delivered via AJAX");
    $commands = array();
    $commands[] = ajax_command_append($wrapper, $output);
    $page = array('#type' => 'ajax', '#commands' => $commands);
    ajax_deliver($page);
  }
  else {
    $output = t("This is some content delivered via a page load.");
    return $output;
  }
}

Thoughts?

Edit: I have a sneaking suspicion I was being stupid with this comment, and that the menu callback should just be returning a renderable array and the js will use the wrapper specified in the #ajax array to append it to. Need to do more testing to see if that's how it works. It certainly makes no sense whatsoever to specify a wrapper in the php, pass it to the js, which passes it back to the php, which eventually passes it back to js again :-P

katbailey’s picture

FileSize
11.73 KB

Here's a re-rolled patch that takes in sun's points in #10 (assuming I understood the last one correctly).

We still need #645800: ajax_deliver() ignores #ajax['method'] and incorrectly forces 'replaceWith' for simple AJAX callbacks, D6->D7 regression in order to have this fully functional so I'll see if I can get that done tonight.

RobLoach’s picture

Did a couple things to it:

  1. You can now change the URL that the browser hits by using $form['link']['#ajax']['path'], so it's not forced to the same URL as $form['link']['#href']. This allows us to get rid of having "nojs" in all the links.
  2. Moved the conditional over to a switch statement to clean up the code a bit. Huge if/elses can get hairy.
  3. Created examplesmodule-ajaxlink.patch off of the Examples module to demonstrate using #type link with a [ajax][path] element.

Would progress effects work on this bad boy?

RobLoach’s picture

+++ includes/ajax.inc	26 Apr 2010 09:51:48 -0000
@@ -464,37 +467,62 @@ function ajax_process_form($element, &$f
+        // Force an ID on the link.
+        if (!isset($element['#attributes']['id'])) {
+          if (isset($element['#id'])) {
+            $element['#attributes']['id'] = $element['#id'];
+          }
+          else if (isset($element['#options']['attributes']['id'])) {
+            $element['#attributes']['id'] = $element['#options']['attributes']['id'];
+          }
+          else {
+            $element['#attributes']['id'] = $element['#id'] = drupal_html_id('ajax-link');
+          }
+        }

+++ includes/common.inc	26 Apr 2010 09:52:11 -0000
@@ -4716,6 +4716,12 @@ function drupal_pre_render_conditional_c
 function drupal_pre_render_link($elements) {

Should we move this into drupal_pre_render_link() ? I guess not, because all links don't need IDs. But, you should still be able to use #id on link types if it's not using #ajax.

Powered by Dreditor.

Status: Needs review » Needs work

The last submitted patch, examplesmodule-ajaxlink.patch, failed testing.

RobLoach’s picture

Status: Needs work » Needs review

The test actually passed.

sun’s picture

Status: Needs review » Needs work
+++ includes/ajax.inc	26 Apr 2010 09:51:48 -0000
@@ -464,37 +467,62 @@ function ajax_process_form($element, &$f
+    // Some AJAX types behave differently.

This comment can be removed -- it does not provide any useful information that wouldn't be obvious from the following code :)

+++ includes/ajax.inc	26 Apr 2010 09:51:48 -0000
@@ -464,37 +467,62 @@ function ajax_process_form($element, &$f
+        // This is a link so we use its href as the url for our AJAX request.
+        // At the same time, however, this can be overriden by the path option.
+        $settings['url'] = isset($settings['path']) ? url($settings['path']) : url($element['#href'], isset($element['#options']) ? $element['#options'] : array());
+        unset($settings['path']);

We can shorten the comment to state that #ajax][path has precedence over #href.

Additionally, I'd prefer a proper if/else condition here, whereby we only unset() in the case of #ajax][path.

+++ includes/ajax.inc	26 Apr 2010 09:51:48 -0000
@@ -464,37 +467,62 @@ function ajax_process_form($element, &$f
+        // Force an ID on the link.
+        if (!isset($element['#attributes']['id'])) {
+          if (isset($element['#id'])) {
+            $element['#attributes']['id'] = $element['#id'];
+          }
+          else if (isset($element['#options']['attributes']['id'])) {
+            $element['#attributes']['id'] = $element['#options']['attributes']['id'];
+          }
+          else {
+            $element['#attributes']['id'] = $element['#id'] = drupal_html_id('ajax-link');
+          }
+        }

Same as mentioned below: #options['attributes'] should have precedence. This gets a bit hairy, but I also don't see another way. Priorities I imagine:

1) Take #options][attributes][id if set, and copy it over to #attributes][id and #id.

2) Take #attributes][id if set, and copy it over to #options][attributes][id and #id.

3) Take #id if set, and copy it over to #options][attributes][id and #attributes][id.

4) Generate a new "#ajax-link" id, and set it to all three properties.

+++ includes/ajax.inc	26 Apr 2010 09:51:48 -0000
@@ -464,37 +467,62 @@ function ajax_process_form($element, &$f
+  ¶

Trailing white-space.

+++ includes/common.inc	26 Apr 2010 09:52:11 -0000
@@ -4716,6 +4716,12 @@ function drupal_pre_render_conditional_c
 function drupal_pre_render_link($elements) {
   $options = isset($elements['#options']) ? $elements['#options'] : array();
+  if (isset($elements['#attributes'])) {
+    $options['attributes'] = $elements['#attributes'];
+    if (isset($elements['#options']['attributes'])) {
+      $options['attributes'] += $elements['#options']['attributes'];
+    }
+  }

#options should have precedence over #attributes, because #options['attributes'] is the "official" attribute that is supported and gets passed. We only need to support #attributes additionally, because its the default location for AJAX and form API.

What I meant above is that it would perhaps be worth to set the resulting/effective #options back into $elements, so any subsequent #pre_render/#theme/etc. can access it -- i.e. instead of populating an anonymous/private $options variable here, we populate $elements['#options'].

96 critical left. Go review some!

katbailey’s picture

Status: Needs work » Needs review
FileSize
12.82 KB

Here we go... however I noticed that in ajax_process_element where we force an id on the link, we're allowing for the possibility that $element['#id'] might not have been set, but this would cause an error earlier in the process when we are creating the settings array (line 461 of ajax.inc):

    // Assign default settings.
    $settings += array(
      'selector' => '#' . $element['#id'],
      'effect' => 'none',
      'speed' => 'none',
      'method' => 'replace',
      'progress' => array('type' => 'throbber'),
    );

So, do we just assume that $element['#id'] has in fact been set and not include the other possibility when we're doing the 'Force an ID on the link' bit? Or do we need to re-jig this code to do the check further up?

sun’s picture

+++ includes/ajax.inc	27 Apr 2010 00:28:16 -0000
@@ -464,37 +467,68 @@ function ajax_process_form($element, &$f
+        // Force an ID on the link.

Yes, we need to move this code back up to where I put it originally, so $element['#id'] is properly set. The tests only passed, because there is no test for #type link using #ajax yet.

+++ includes/ajax.inc	27 Apr 2010 00:28:16 -0000
@@ -464,37 +467,68 @@ function ajax_process_form($element, &$f
+        else if (isset($element['#attributes']['id'])) {
...
+        else if (isset($element['#id'])) {

Coding standard is "elseif" (without space).

+++ includes/ajax.inc	27 Apr 2010 00:28:16 -0000
@@ -464,37 +467,68 @@ function ajax_process_form($element, &$f
+        }
+
+        break;

Stale blank line here.

96 critical left. Go review some!

RobLoach’s picture

Status: Needs review » Needs work

Yes, we need to move this code back up to where I put it originally, so $element['#id'] is properly set. The tests only passed, because there is no test for #type link using #ajax yet.

It won't work beside $element['#ajax']['event'] = 'click'; because that is just setting the default event if "event" is not set. If you change the event to "mouseover", for example, the force ID code would not run and the element would not have an ID.

The other two points are valid though.

sun’s picture

Status: Needs work » Needs review
FileSize
12.52 KB

Ah, sure, you're right. Wasn't visible from the patch context.

So this one should be ready to fly.

Status: Needs review » Needs work

The last submitted patch, drupal.ajax-link.21.patch, failed testing.

katbailey’s picture

FileSize
12.82 KB

The crazy id assignment stuff breaks one of the form tests; making sure we only do that for #type 'link' (which after all is how it's explained in the comment) stops said test from failing.

katbailey’s picture

Status: Needs work » Needs review
sun’s picture

Status: Needs review » Reviewed & tested by the community

Something is wrong with the testbot, but this patch looks very good to me. Not particularly happy about the #type 'link' special-casing, but we can make this more generic when a requirement to do so comes up in the future; can't think of other elements currently.

Thanks!

moshe weitzman’s picture

#23: drupal.ajax-link.23.patch queued for re-testing.

Dries’s picture

Why should this be considered for Drupal 7?

katbailey’s picture

@Dries, to me at least, it seems like a huge oversight if this doesn't get in: when the AJAX framework came into core from CTools, pretty much all further development on it focused on AJAX forms as this was by far the most challenging aspect of it. The ability to load content via ajax by clicking on a link (completely unrelated to forms) was there, but undocumented and unused. CTools for D6 has continued to develop both aspects of its ajax framework (form-related and otherwise), for example providing a solution to the problem of lazy-loading css and js for ajax-loaded content. Meanwhile in core, such development (e.g. #561858: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework) focuses only on forms.

In D6 a lot of confusion arose from the fact that the term "AHAH" referred to a very specific type of functionality, namely dynamically loading form elements. We're in danger of doing the same thing all over again in D7, where the AJAX framework is actually just an AJAX forms framework, and the vast multitudes of developers who are trying to do a very basic thing like loading a node or a View via ajax will be confounded to eventually realise that it doesn't provide them much help at all. Of course, if #561858: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework doesn't make it in, then this becomes less relevant.

That's my perspective on it anyway. It would be good to get merlinofchaos's view.

merlinofchaos’s picture

I agree with Kat in the sense that core's focus on AJAX for forms is not where, IMO, the real power of AJAX lies. ahah.js was developed to allow us to do cute tricks like adding more options to the poll options dynamically, or being able to deal better with book navigation.

ajax.js was designed to create powerful, robust AJAX interfaces where forms are subservient to the system, not the other way around. With the fapi renderer being genericized and used more across Drupal, it is good to keep that goal of the system in mind. Making it convenient to add AJAX functionality to links wherever they are rendered will allow authors to add robust AJAX functionality easily. Right now, the framework still has a few limitations that are going to prevent that. This is a nice API improvement that won't break pre-existing APIs at all, so it's a good way to keep THIS side of the ajax framework up with the work that's been done on forms.

IMO it's not as important as #561858: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework which I think will be necessary to allow AJAX interfaces to truly shine.

sun’s picture

Category: task » bug

This needs to be considered for D7, because this issue was created after a contributed module tried and failed to use the new AJAX framework in D7. Actually, it's a bug report.

fago’s picture

Status: Reviewed & tested by the community » Needs work

Generally the #ajax system is currently not usable with #link, neither with or without forms. I agree, that this is a bug that needs to be fixed - but to work in both cases (also see #753270: Impossible to trigger form submit with a link)

+function ajax_process_element($element, &$form_state = array()) {
   // Nothing to do if there is neither a callback nor a path.
-  if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) {
+  if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']) || isset($element['#href']))) {
     return $element;
   }

Why is the element skipped in case $element['#href'] is set? Furthermore $element['#href'] is used afterwards in the function, which together makes no sense at all.

* Also renaming ajax_process_form() to ajax_process_element() is an API change, as any form element provided by contrib making use of the #ajax system has to adapt. Maybe we could keep the old function name, which makes use of the new ajax_process_element() but additionally covers FAPI stuff, thus enables caching?

* I like the #id syncing part - having different ways to specify the id is awkward. Till now I had to define the same id using two different ways at the same time to get it working for my use case, so +1 to fix that.
However drupal_pre_render_link() doesn't take over the id specified via #id, but the #ajax handling of links does. Thus if there is no #ajax, #id would stop working. That's inconsistent, #id should be taken into account regardless whether #ajax stuff is there.

* With the patch ajax processing of a link is moved to pre_render - but what if the link is used in a form? Then processing won't turn on form caching, but it would basically work - weird. Imo we should also add the usual ajax_process_form() as #process handler and do nothing in ajax_process_element() in case the element has been #processed - therefore assuming ajax_process_form() has been invoked then.

sun’s picture

1) It's the opposite, as the entire condition is negated. One of the three properties must be set to continue.

2) I wouldn't be concerned about changing the name of this #process function. I didn't grep D7 contrib, but I'd be surprised if any module would actually specify it. Since it's no longer a FAPI-only function, but a RAPI preprocessor instead, it would be good to name the function accordingly.

3) is impossible to do without introducing yet another callback function just for the #id. Bad for performance. If you want an ID, then simply assign 'attributes']['id', which works for everything.

4) We'd have to specify both a #process and a #pre_render, and somehow identify whether the element was already processed via #process. I'm not sure whether it makes sense to use a link within a form to invoke actual form processing in the first place though -- how do you invoke the same action without JavaScript being enabled? So, from a logical standpoint, I'd say that this point is moot...?

fago’s picture

1) arg, I see.

2) Well, I did. I'm surely not the only one. My suggestion was to build the FAPI function around the RAPI preprocessor - so we would have an appropriate function name for the RAPI preprocessor + BC.

3) What's the problem with taking #id into account in drupal_pre_render_link()?

4) FAPI sets $element['#processed'] = TRUE; - it should be as simple as checking that. For degrading from the link in forms I simply use the trigger_as functionality of #ajax, which makes it easy for a link to degrade to a button (hide the button by JS, show the link by JS). I use it to switch parts of the form, having buttons for that visually clutters the form too much.

effulgentsia’s picture

Component: javascript » ajax system

Subscribe. Will hopefully find some time to read this and have something useful to say about it, but please don't hold it up on my account.

rfay’s picture

subscribe

katbailey’s picture

Hi All,
I hope it's not too late to breathe new life into this and get it in, it seems we're so close. I made the change that fago was looking for, i.e. splitting it out into the FAPI function ajax_process_form, which in turn calls ajax_process_element. I have not tested it with his particular use case, i.e. having a link that submits a form. I've been testing this using ajax_example module and am attaching a patch to that that modifies the ajax link example to use a rendered array with the #ajax property for the link. I hope some of you have a chance to test this out soon.

katbailey’s picture

Status: Needs work » Needs review

Forgot to change the status.

Status: Needs review » Needs work

The last submitted patch, ajax_link_example.patch, failed testing.

sun’s picture

I thought this would've been committed years ago? Unfortunately, quite some failures for the last patch.

mfer’s picture

subscribe

katbailey’s picture

Wow, 90 fails! And unrelated to ajax stuff it looks like at first glance. Weird - I'll try and get to it today and see what's going on there.

rfay’s picture

Status: Needs work » Needs review

#36: drupal.ajax-link.36.patch queued for re-testing.

katbailey’s picture

FileSize
10.37 KB

Oops, stupid mistake in the last patch (yay for tests!), this one should be good...

sun’s picture

Title: Make #type 'link' ajaxifiable » #type 'link' is not ajaxifiable
FileSize
10.25 KB

Fixed a couple of comments, and renamed ajax_process_element() into ajax_pre_render_element(), as it's a #pre_render callback now.

Additionally, I've introduced #ajax_processed as indicator for successful AJAX processing (i.e., something was done). And used that to replace the form caching condition in ajax_process_form(). I think we also have to enable form caching for links, to be on the safe side. We don't know yet what kind of weird things people are going to build with all of this stuff.

sun’s picture

I've manually tested this patch again today. Still works like a charm. Looks RTBC to me. Any other feedback?

sun’s picture

FileSize
10.25 KB

Re-rolled against HEAD. Hey, feedback, anyone?

a25i’s picture

#46: drupal.ajax-link.46.patch queued for re-testing.

sun’s picture

FileSize
14.41 KB

So I've researched and analyzed jquery.form.js' code a bit and it turns out that we can totally easily abstract the required AJAX POST value handling in ajax.js.

Attached patch additionally contains the follow-up patch from #850612: setting class 'use-ajax-submit' for form submission causes js error(s) - sorry for that.

This patch effectively makes http://drupalcode.org/viewvc/drupal/contributions/sandbox/sun/core_tabs/ work again! :)

sun’s picture

Assigned: katbailey » sun

Sorry, Kat, taking this over for now (unless you beat me to it) ;)

rfay’s picture

OK with me as long as the code gets demonstrated in action and tested in real life (and your core_tabs is a good thing :-)

sun’s picture

Status: Needs review » Needs work
+++ misc/ajax.js	5 Oct 2010 02:27:23 -0000
@@ -216,31 +218,33 @@ Drupal.ajax.prototype.beforeSerialize = 
+  options.data.ajax_page_state = { css: {}, js: {} };
...
   for (var key in Drupal.settings.ajaxPageState.css) {
-    form_values.push({ name: 'ajax_page_state[css][' + key + ']', value: 1 });
+    options.data.ajax_page_state.css[key] = 1;
   }
   for (var key in Drupal.settings.ajaxPageState.js) {
-    form_values.push({ name: 'ajax_page_state[js][' + key + ']', value: 1 });
+    options.data.ajax_page_state.js[key] = 1;
   }

That apparently looks like nonsense. Seems like we can skip the entire for...in loop and just use the global Drupal.settings variables ;)

Powered by Dreditor.

sun’s picture

Status: Needs work » Needs review
FileSize
13.13 KB

So here comes a heavily simplified patch. A minor change to the new lazy-loading code allows us to pass any ajax page state data to the server, which is extremely nice, as that makes it modular.

This patch is RTBC based on my testing.

sun’s picture

Priority: Normal » Major

To my knowledge, this is the last blocker for modules that are trying to use the AJAX framework. And since this is actually blocking a couple of modules from being ported to D7, bumping to major.

sun’s picture

Status: Needs review » Needs work

Latest patch breaks lazy-loading. options.data doesn't support objects, so um, yeah, I'll figure out something.

sun’s picture

Status: Needs work » Needs review
FileSize
13.37 KB

Attached patch fixes the primary bug. Leaving us with fixing broken merge of ajaxPageState in subsequent AJAX requests.

merlinofchaos’s picture

There is a major problem with this:

When using $.ajaxSubmit, the page state is being sent as:

ajax_page_state	[object Object]

Rather than the contents of the object. Maybe there needs to be a serialize process that isn't happening?

sun’s picture

FileSize
14.4 KB

Very good finding, merlin! Attached patch fixes that, too. :) So this one should make everything work correctly.

EDIT: Actually, I meant your reply on the other issue ;)

sun’s picture

+++ includes/common.inc	5 Oct 2010 23:18:25 -0000
@@ -4008,9 +4008,11 @@ function drupal_get_js($scope = 'header'
   // Provide the page with information about the individual JavaScript files
   // used, information not otherwise available when aggregation is enabled.
-  $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1);
-  unset($setting['ajaxPageState']['js']['settings']);
-  drupal_add_js($setting, 'setting');
+  if (!$skip_alter) {
+    $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1);
+    unset($setting['ajaxPageState']['js']['settings']);
+    drupal_add_js($setting, 'setting');
+  }

So this is the actual change that prevents Drupal.settings.ajaxPageState from being overwritten in subsequent AJAX requests.

We likely want to rename the function argument from $skip_alter to $ajax or similar, as the current state of affairs is obviously non-obvious. ;)

Powered by Dreditor.

merlinofchaos’s picture

It seems better to just unset the settings prior to calling drupal_get_js() in ajax_render(), rather than using a special parameter to drupal_get_js(). At least, to me.

// Settings are transmitted separately so they can be merged, so remove them
// from the general javascript transmitted here:
unset($items['js']['settings']);
$scripts_footer = drupal_get_js('footer', $items['js'], TRUE);
$scripts_header = drupal_get_js('header', $items['js'], TRUE);
?>
effulgentsia’s picture

sun’s picture

FileSize
14.06 KB

OK, that's true and much better.

sun’s picture

I'm still testing with rfay's excellent lazy_load_demo module from http://drupal.org/node/561858#comment-3352146 as well as my own core_tabs demo module (linked above) and everything seems to work perfectly with this patch.

One remaining bug that's not to be solved in this issue is that subsequent AJAX requests needlessly lead to new HTML IDs on content that is replaced. I already mentioned in #926016-21: Several bugs when trying to remove files from a multivalued file/image field that our current handling of #ajax][wrapper is not sufficient, as the wrapper HTML ID will change. This either means that we need a server-side facility to pass the right wrapper element #id to the client, or some other way. Perhaps, the aforementioned issue can be slightly re-purposed to tackle this not only for File module. You can reproduce the bug with rfay's lazy_load_demo module by toggling the "Enable #states behavior" button at least 3 or more times and then click the #states-driven checkbox -- it fails to apply, because the HTML IDs differ.

sun’s picture

FileSize
13.66 KB

Re-rolled against HEAD. Lazy-loading has been fixed in the original issue. This patch still fixes lazy-loading for non-form AJAX and lastly, makes #type link compatible with #ajax.

sun’s picture

Any feedback? This patch looks RTBC to me.

effulgentsia’s picture

I will look at this thoroughly later today. I'd love to know what others who participated in this issue think of #63 as well.

effulgentsia’s picture

Status: Needs review » Needs work

Very nice. Overall, I really like the patch. Seems to exactly solve what is needed, and is backwards compatible, so no reason for it not to go into D7. Nits:

+++ includes/ajax.inc	6 Oct 2010 23:31:15 -0000
@@ -515,7 +515,25 @@ function ajax_footer() {
+ *   An associative array containing the properties of the element. See
+ *   ajax_process_element().

See ajax_pre_render_element()?

+++ includes/ajax.inc	6 Oct 2010 23:31:15 -0000
@@ -531,12 +549,16 @@ function ajax_footer() {
+  // Skip elements already processed by Form API via ajax_process_form().
+  if (isset($element['#processed']) && $element['#processed']) {
+    return $element;
+  }

Why #processed? Shouldn't it be #ajax_processed? Suppose the link element has a #process function that isn't ajax_process_form().

+++ includes/ajax.inc	6 Oct 2010 23:31:15 -0000
@@ -579,6 +605,30 @@ function ajax_process_form($element, &$f
+    // When being invoked outside of Form API context, elements of #type 'link'
+    // do not have a HTML ID, unless manually set in #options['attributes'],
+    // #attributes, or #id. Either take over one of those or generate a new one.
+    // @see drupal_pre_render_link()
+    if (isset($element['#type']) && $element['#type'] == 'link') {
+      if (isset($element['#options']['attributes']['id'])) {
+        $element['#id'] = $element['#options']['attributes']['id'];
+        $element['#attributes']['id'] = $element['#options']['attributes']['id'];
+      }
+      elseif (isset($element['#attributes']['id'])) {
+        $element['#id'] = $element['#attributes']['id'];
+        $element['#options']['attributes']['id'] = $element['#attributes']['id'];
+      }
+      elseif (isset($element['#id'])) {
+        $element['#options']['attributes']['id'] = $element['#id'];
+        $element['#attributes']['id'] = $element['#id'];
+      }
+      else {
+        $element['#id'] = drupal_html_id('ajax-link');
+        $element['#options']['attributes']['id'] = $element['#id'];
+        $element['#attributes']['id'] = $element['#id'];
+      }
+    }
+

How about this instead?

// @todo In Drupal 8, standardize on using #id is the property for specifying
//   an HTML ID for all element types, including links. Until then, for link
//   elements, values in #options and #attributes take precedence.
// @see drupal_pre_render_link()
if (isset($element['#type']) && $element['#type'] == 'link') {
  if (isset($element['#options']['attributes']['id'])) {
    $element['#id'] = $element['#options']['attributes']['id'];
  }
  elseif (isset($element['#attributes']['id'])) {
    $element['#id'] = $element['#attributes']['id'];
  }

  // During form building, form_builder() ensures that all elements within the
  // form have a #id value, but AJAX can be attached to links that are not part
  // of a form.
  if (!isset($element['#id'])) {
    $element['#id'] = drupal_html_id('ajax-link');
  }

  // Ensure that all of the variables containing the HTML ID are consistent.
  $element['#options']['attributes']['id'] = $element['#attributes']['id'] = $element['#id'];
}

Not necessary, just a suggestion. I fear the code as-is will be a red flag to Dries/webchick.

+++ includes/ajax.inc	6 Oct 2010 23:31:15 -0000
@@ -644,8 +709,8 @@ function ajax_process_form($element, &$f
+    // Allow others to identify whether something was successfully processed.
+    $element['#ajax_processed'] = TRUE;

Minor, but we use 'process' an awful lot, so it gets confusing. How about '#ajax_attached'?

+++ misc/ajax.js	6 Oct 2010 23:32:08 -0000
@@ -176,6 +176,7 @@ Drupal.ajax = function (base, element, e
+        ajax.beforeSerialize(ajax.element, options);

Should this be changed to ajax.beforeSerialize(ajax.element, ajax.options);?

Also, we need an example added to the docs at the top of ajax.inc, right?

And a test would be nice: at least one that checks Drupal.settings has what's expected for a link inside a form and a link outside a form.

Finally, I haven't tested this. @merlinofchaos, @katbailey: after above feedback is included, can you confirm it works for your use-cases?

sun’s picture

Status: Needs work » Needs review
FileSize
13.69 KB

Re-rolled with the easy changes.

- The insanity of #options, #attributes, and #id is something I don't think we can get around... we simply happen to support all of those elsewhere, in different sub-systems, so IMHO it would be illogical if #ajax would only account for one of all possible ways. The current code checks each possible property separately and takes over its value for the other possible properties accordingly. Kind of a mess, I agree, but that's what we have to deal with until D8+.

- Yes, we definitely need tests.

effulgentsia’s picture

Title: #type 'link' is not ajaxifiable » Links are needlessly unable to fully participate in D7 AJAX framework features
Priority: Major » Critical

Following up on #561858-208: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework, making this issue critical. Summed up well in #27-#29. Basically, there are two ways currently in HEAD, in which links are given 2nd class status with respect to AJAX:

1) They can't have #ajax settings, and therefore, can't specify things like 'wrapper', 'path', 'progress', etc., the way that form elements can. All they can have in HEAD is a "use-ajax" class, which uses defaults for all settings only. There's no good reason for this limitation. It came about historically, because when the D7 AJAX framework first went in, the generalization of parts of FAPI into a render API were still in progress, and we didn't have a #type => 'link' element type. Now we do, so it makes a lot of sense to make it a first class citizen, especially since there's no BC break in doing so.

2) They don't benefit from the lazy loading added in the other issue, because for no good reason, we put the magic JS code into beforeSubmit(), which only runs for form submissions, instead of into beforeSerialize(), which runs for both form submissions and link submissions. #67 fixes that.

I gave examples in #561858-207: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework, in addition to what's been written in #28 and #29, for why there's lots of good reasons for links to have full participation in the AJAX framework.

Discussing #67 with sun now, and one of us will follow up with another patch.

sun’s picture

FileSize
9.93 KB

Discussing extensibility and flexibility of ajax_pre_render_element() a bit more with eff and merlin, we came to the attached conclusion and simplification.

I'll try to simplify those giant if/else conditions in drupal_pre_render_link()... somehow.

sun’s picture

FileSize
9.67 KB

Heavily simplified drupal_pre_render_link().

Status: Needs review » Needs work

The last submitted patch, drupal.ajax-link.70.patch, failed testing.

sun’s picture

Status: Needs work » Needs review
FileSize
11.96 KB

Added tests. Really wanted to do more to partially cover the lazy-loading, but that requires API changes, so leaving that to a separate issue.

Status: Needs review » Needs work

The last submitted patch, drupal.ajax-link.71.patch, failed testing.

sun’s picture

Status: Needs work » Needs review
FileSize
13.09 KB

#type 'link' in a form automatically gets a HTML ID like any other form element now, which is naturally expected, and which explains the two failing assertions. Thanks eff for helping to figure this out! :)

sun’s picture

FileSize
13.09 KB

oopsie :)

Status: Needs review » Needs work
Issue tags: -Ajax

The last submitted patch, drupal.ajax-link.75.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
Issue tags: +Ajax

#75: drupal.ajax-link.75.patch queued for re-testing.

effulgentsia’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Ajax

Code looks great. Summary in #68. Simpletest added for #68.1. Simpletest not possible yet for #68.2, but as per #561858-201: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework, I will work on enhancing drupalPostAJAX() to be able to pseudo-test that at some point. In the meantime, the example module in #561858-207: [Tests added] Fix drupal_add_js() and drupal_add_css() to work for AJAX requests too by adding lazy-load to AJAX framework can be used for manually testing that, and shows that #75 works perfectly.

Hard-coded special-casing for 'link' within ajax_pre_render_element() that were part of earlier patches has been removed, achieving merlinofchaos's request that this be extensible to custom types (e.g., 'ctools_link').

Therefore, RTBC.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Committed to CVS HEAD. Thanks!

kika’s picture

What about docs / upgrade guide? Is it needed?

sun’s picture

Status: Fixed » Needs review
FileSize
1.03 KB

Quick follow-up fix: At least in Firefox, after clicking an ajaxified link, a disabled="false" HTML attribute remains on the anchor element, which prevents it from being clicked again.

Attached patch fixes that. You may test with http://drupalcode.org/viewvc/drupal/contributions/sandbox/sun/core_tabs/

sun’s picture

EvanDonovan’s picture

Is the followup still critical?

chx’s picture

Status: Needs review » Reviewed & tested by the community

Yes, but it's small and easy.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Committed the follow-up. Thanks.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.