Loading tab content via Ajax #3

Closed
htowens opened this Issue May 18, 2011 · 23 comments

2 participants

@htowens

Hi,

A quick clarification - in the standard UI tabs, setting an href value on the tab link will allow the tab content to be loaded via ajax. The homepage for this project indicates that it has all the functionality of the ui tabs, but also specifies that it does not do ajax loading.

Thus, my question is, if I drop this in place of my normal UI tabs, which load with ajax, will it break the ajax loading? If so, is there any workaround for this?

Thanks for any assistance!
Henry.

@JangoSteve
Owner

It doesn't currently do ajax tabs (yet). Unbelievably, no one has requested it yet, so I just never got around to building that in. I could probably have that in the next version.

In the meantime, you could do something like this:

<div id="container">
  ...
  <a href="#panel-1" data-url="/some/url/path" class="tabs">I'm a tab</a>
  ...
  <div id="panel-1"></div>
  ...
</div>

And then in your javascript, having something like:

$('#container').easytabs();
$('.tabs').click(function(){
  var $this = $(this);
  $($this.attr('href')).load($this.data('url'));
});
@htowens

Hi Steve,

I'm amazed nobody requested it - really easy ajax loading is one of the reasons I use the jquery tabs at present, but i've been looking for something that is a bit more customizable in terms of look and feel.

That workaround looks good in the meantime (with the addition of a bit of loading script - do your tabs have events that I can bind to for this also?) - it should at least let me get the tabs integrated while I eagerly await the version with ajax built in!

Thanks for the quick response,
Henry.

@JangoSteve
Owner

I hear ya, that's exactly what prompted me to build easytabs, was the difficulty in customizing the jquery-ui tabs. I've never needed the ajax stuff myself, but yeah, I thought someone would have requested it long ago ;-)

And yes, there are event hooks you can bind into. See http://www.alfajango.com/blog/jquery-easytabs-plugin/#event-hooks

You can also cancel the tab-change by binding a handler function onto the easytabs:before hook. See http://www.alfajango.com/blog/jquery-easytabs-plugin-v2-1-2/#cancel-before-hook

@JangoSteve
Owner

Oh, and now that you mention it, an even better way to load the ajax data might be to bind to the easytabs:before event hook rather than the click event. When EasyTabs goes to change tabs, it first fires that event and waits for any handler functions bound to it to finish, before it switches tabs. So you could do:

$('.tabs').bind('easytabs:before', function(){
  var $this = $(this);
  return $($this.attr('href')).load($this.data('url'));
});

Now, the jQuery .load() function is non-blocking, so that may not actually delay the tab-change until the content is loaded (as would be desired), but you get the idea.

@htowens

I think the tab changing before the content is fully loaded is OK actually - if it is loading a lot of data for whatever reason and the request takes a while, I'd rather have the tab change (and display a loading indicator in the tab contents using the same binding, until easytabs:after fires, at which point I can bind to it and show the data). If the tab doesn't change, I think the user might try clicking again etc. thinking nothing has happened. Or maybe I just have impatient users.

@JangoSteve
Owner

Yeah, I was thinking you'd maybe want to put a loading indicator on the tab or in the panel immediately, and then remove the loading indicator and change the tab when it's loaded. But your way is equally valid.

@JangoSteve
Owner

OK, I added ajax functionality to EasyTabs and pushed it up to github (I want to hold off releasing an official version until it's better tested). If you'd like to give it a try, download the JS file here.

I wanted to keep it semantic, so that it degrades gracefully into a normal link for browsers without javascript. So I did it differently than the jquery ui tabs. To accomplish this, I kept the ajax URL in the tab's href attribute. To make a tab load data via ajax, do this:

<div id="container">
  ...
  <a href="/some/ajax/path.html" data-target="#panel-1" class="tabs">I'm a tab</a>
  ...
  <div id="panel-1"></div>
  ...
</div>

EasyTabs will automatically pick up any tabs with a data-target attribute that matches a panel id on the page, without any special configuration, and treat them as ajax tabs.

You can also load a page fragment by adding a space to the href followed by a jquery selector. E.g.

<a href="/some/ajax/path.html #some-element" data-target="#panel-1" class="tabs">I'm a tab</a>

There is also now an optional cache option which tells easytabs whether to load the ajax url every time the tab is clicked, or just the first time. cache = true by default. So, you can force the ajax to be reloaded every time with:

$('#container').easytabs({ cache: false });

Let me know how it works.

@htowens

Wow, that was fast - I didn't even have a chance to test the workaround!

I'll plug this in this evening (BST) and let you know how I get on.

The ability to use a page fragment looks nice, I hadn't even thought about that - is the syntax the same for loading a partial?

Thanks!

@htowens

Oops, should have asked also - do I need to modify the JQuery to introduce a loading gif, or is that built in now?

@JangoSteve
Owner

Ah very good question. So I just pushed out another update with two new hooks to help with the loading UI, easytabs:ajax:beforeSend and easytabs:ajax:complete. You can now use them to add a loading indicator like this:

$('#container')
    .bind('easytabs:ajax:beforeSend', function(e, clicked, panel){
      var $this = $(clicked);
      $this.data('label', $this.html());
      $this.html('Loading...');
    })
    .bind('easytabs:ajax:complete', function(e, clicked, panel, response, status, xhr) {
      var $this = $(clicked);
      $this.html($this.data('label'));
      if (status == "error") {
        var msg = "Sorry but there was an error: ";
        $("#error").html(msg + xhr.status + " " + xhr.statusText);
      }
    });
@htowens

Apologies for the delay in getting this tried out - some work commitments came up unexpectedly.

I have implemented the new js, and all appears to be working well at this point. I haven't done any cross-browser testing yet, but in Chrome all is looking good; the console shows a request for the active tab only when the page loads, and requests the content for the other tabs only once the tab is opened.

I'll update further once I have done more extensive testing.

One thing I did notice is that I initially tried to put my includes in my application layout, but found that it was causing the ajax requests to loop and crash the server - looks like the includes always need to be in the view?

@JangoSteve
Owner

Awesome, glad it's working well. Yes, the fact that it's only loading the content the first time the tab is clicked is intentional. If you would like it to reload the content every time the tab is clicked, set the cache option to false:

$('#tab-container').easytabs({cache: false});

I'm not sure what you are referring to by "includes".

@htowens

Steve,

I knew it was intentional - I think I may have been a little ambiguous in the way I wrote the last comment, I was mentioning that the console was showing a request only once when the page loads to show that it was working, not implying that it was not!

By includes, I meant the js references in the view (includes was a bad choice of terminology again) - the items like:

<script src="/javascripts/jquery.js" type="text/javascript"></script> 
<script src="/javascripts/jquery.hashchange.min.js" type="text/javascript"></script> 
<script src="/javascripts/jquery.easytabs.js" type="text/javascript"></script>  
<script type="text/javascript"> 
  $(document).ready(function(){ $('#tab-container').easytabs(); });
</script>

One thing I have just noticed is some strange behaviour with the hashchange plugin - I initially only had one content div (i didn't need any more since the content is getting replaced), like so:

<div id="tab-container">
  <ul>
    <li><a href="/tab1contents" data-target="#content-div">Tab 1</a></li>
    <li><a href="/tab2contents" data-target="#content-div">The Second Tab</a></li>
    <li><a href="/tab3contents" data-target="#content-div">Tab C</a></li>
  </ul>
  <div id="content-div"></div>
</div>

But this clearly causes a hashchange issue, since it only shows http://localhost:3000/#content-div. I then tried adding extra content divs, with each tab pointing to a different div; this works to allow the address to change in the address bar, but it does not change the active tab, or the content.

I have little familiarity with the hashchange plugin, and as such do not have an understanding of why this might happen - can you suggest any possibilities?

@JangoSteve
Owner

Ah I gotcha, well awesome then :-)

I'm not sure why putting the script tags in the application layout would cause any problems, that's where I always put mine. Unless of course you're rendering the entire application layout in your ajax response, which is probably a bad thing. I'd make sure that in your controller action for the ajax URLs, you respond with something like:

respond_to do |format|
  format.html { render :layout => (!request.xhr?) }
end

Which will respond with :layout => false if the request is an ajax request.

Yeah, with the panel divs, I'm not sure. EasyTabs is really designed from the ground up to have a one-to-one mapping between tabs and panels, so I'm not sure what kind of funkiness happens when that isn't the case. I'd probably just go ahead and make one panel per tab to avoid that weirdness.

@htowens

Yeah, the ajax loading seems to break hashchange altogether - even with separate divs, it still doesn't change the active tab or load the contents (though the address in the bar does change).

I think I'll need to do some more research into the workings of hashchange before I can make a stab at fixing it or concocting something else that works for the ajax tabs.

@JangoSteve
Owner

Oh, I understand now with the hashchange stuff. I was able to reproduce and fix it. AJAX tabs should be working with hashchange (forward and back buttons) as of this commit.

@htowens

Steve,

Awesome, the hashchange stuff seems to be working well now.

Another query (if you would like me to move to a separate issue, please let me know) - is it possible to nest sets of easytabs? E.g. I want to have my main tabs, and some subtabs in each of the main tabs, which can also load content via ajax.

I tried a simple nesting by simply putting a second set of tabs within the layout of the page being loaded by the main tab (with different container div ids), but I got some unexpected behaviour, such as the main set of tabs trying to load content that should have been loaded by the subtabs, which makes me think it maybe wasn't designed to be used in this way.

Thanks again for all your help - hopefully the project has benefitted from the changes!

Henry.

@JangoSteve
Owner

Awesome, glad it's working.

Yeah, I think I know what the problem is for the nested tabs, and it's no small task, so maybe let's move to a new issue and close this one out. The good news is, it should be doable.

@htowens

Thanks Steve - I'll close this issue down as it's working from my perspective. I'll open a new one for the nesting.

@htowens htowens closed this May 31, 2011
@htowens

I perhaps should have asked for future reference (and also anyone else coming across this thread), can the tabs be used with a rails link_to (where the link is somewhere outside of the main tab ul), or does the link always have to be specific in the href, and within the tab ul?

For example, if I have a link to edit a record, and want this to be loaded in the contents of a tab (i.e. an edit tab), can the <a href=".." data-target="#tabcontents"> be substituted with <a edit_object_path data-target="#tabcontents"> (where the link is not in the tab ul) and have easytabs process this into a tab?

Thanks!

@JangoSteve
Owner

Yes, having link_to "Edit", edit_object_path, :'data-target' => '#tabcontents' should work just fine, as long as the link is included in the tabs selector (specified in the easytabs options) and is somewhere inside the container div that easytabs is called on.

There's also a public easytabs select method which could be used for a link anywhere on the page:

$('#tab-container').easytabs();

$('#some-link').live('click', function() {
  $('#tab-container').easytabs('select', '#tabcontents');
});

<!-- somewhere on the page //-->
<%= link_to "Edit", edit_object_url, :id => 'some-link' %>
@htowens

I've got the selection of the tabs working fine (the tab that I have specified in the js opens correctly), but I seem to be having some issue with getting it to load the url specified in the edit link (all of the tabs are loading content via ajax, so the tab does not have any content initially).

I have this in my js:

$('#editroundlink').live('click', function() {
  $('#subtab-container').easytabs('select', '#editround');
});

And the "edit round" button on my index page looks like:

<%= link_to "Edit", edit_round_path(round), :id => "editroundlink" %>

The intention here is to load the edit form for the round in the edit tab, but the url will be different each time, due to the id changing, so I can't specify it in the tab area. At present, although easytabs is picking up the click event and changing the tab, the page being loaded is whatever is specified in the tab, not the edit_round_path url from the link.

Is there a way to pass this to the easytabs method, to tell it what url to load via Ajax?

@JangoSteve
Owner

There's no way to pass the url directly to easytabs, as it will always read it from the tab href or data-url. However, using jQuery, you could update the target url stored for the tab. Something like:

$('#editroundlink').live('click', function() {
  $('#editround').data('easytabs').ajax = $(this).attr('href');
  $('#subtab-container').easytabs('select', '#editround');
});

Perhaps I should document at some point where all the data info is stored for easytabs on the container and tab elements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment