thinksimple.pl

Bye bye Prototype!

July 27, 2009

I’ve been struggling for a couple of days to make a javascript carousel. Or, to find a library that does that. There are a ton of scripts out there, just grab one of ‘em and let’s get started!

Ughhh. Little or no documentation. Incomplete examples. Lots of HTML and CSS just to have the same thing as in the example and it still doesn’t work. Seriously, is it really that big? In the meantime I saw this page, Top 10 JS Carousels and Sliders. Out of those 10, 5 (namely: five!) were written in jQuery and only two in Prototype.

So today I gave up. I installed jrails. And tried out jCarouselLite as jCarousel seemed too big for my needs. And guess what? I got it up and running in about 10 minutes, including downloading, skimming documentation and updating my HTML (actually, removing all unnecessary markup needed by previous libraries). Worked like a charm. Also, now I’m really really angry that Rails makes you use Prototype by default.

You can see the result on my projects page.

How to make an entry preview

April 11, 2009

UPDATE: there is now a plugin doing exactly what is described here. Read all about with_preview plugin!

If you have a blog you sometimes need to see what your entry will look like before you post it. Although the thing might seem complicated, this is quite easy to achieve.

First, let’s have a form for our blog post:

<% form_for :entry, :html => {:id => :entry_form} do |f| %>
  <%= f.text_field :title %>
  <%= f.text_field :tag_names %>
  <%= f.text_area :content %>
  <%= f.submit 'Create' %>
<% end %>

We will split that form into divs:

<% form_for :entry, :html => {:id => :entry_form} do |f| %>
<div id="entry_edit">
  <%= f.text_field :title %>
  <%= f.text_field :tag_names %>
  <%= f.text_area :content %>
</div>
<div id="entry_preview">
</div>
  <%= f.submit 'Create' %>
<% end %>

Now let’s add a button which will send an AJAX request to server with our form data:

<%= button_to_remote :Preview, {
  :update => :entry_preview, 
  :url => preview_entries_path, 
  :method => :post, 
  :with => "$('entry_form').serialize()", 
  :success => "$('entry_edit').hide(); 
    $('entry_preview').show(); 
    $('preview_button').hide(); 
    $('edit_button').show();"
  }, {:id => :preview_button} %>

The controller action is pretty simple. I want to show the date when the post was created, so I just set up the data in the controller.

def preview
  @entry = Entry.new(params[:entry])
  @entry.created_at = Time.now
  render :layout => false
end

And here is the view for the action:

<div class="preview">
<div class="preview_title">Entry preview</div>
  <h1><%=h @entry.title %></h1>
  <h2><%=h @entry.tag_names %></h2>
  <h3><%=h @entry.created_at.to_s(:short) %></h3>
  <p><%=h @entry.content %></p>
</div>

Okay, by now we can see the preview of the post, but we would also like to be able to get back to editing. This is much simpler. First, we need a button called ‘Edit’, right next to the ‘Create’ and ‘Preview’ buttons. Only this time we don’t need AJAX, plain Javascript will do:

<%= button_to_function :Edit, :id => :edit_button do |page|
  page[:entry_preview].hide
  page[:entry_edit].show
  page[:edit_button].hide
  page[:preview_button].show
end %>

Let’s hide the ‘Edit’ button so that it will appear only after the ‘Preview’ button was pressed:

<script type="text/javascript">
  $('edit_button').hide();
</script>

And finally we need to set up a proper route in config/routes.rb file:

map.resources :entries, :collection => {:preview => :put}

That’s all, you can now admire your previews!

jsonp

June 17, 2009

I have an external application which does its job quite well. It was not written by me, and I feel it’s too complicated for me to mess with it, but lately we had an idea to display a couple of blog articles on the main page. It has been done loads of times, all you need is a simple API, Rails creates that API even in scaffold, I’ll just put an AJAX call on the main page and that’s all, right?

Well, no. There is something called Same Origin Policy (SOP) which only allows AJAX requests to proceed if the called URL is the same as website URL. Obviously, that will not be the case, because our application and the blog are two separate systems, and they are not running under the same URL. Bummer.

Okay then, how do Google Ads work? They’re not on the same domain, and they’re based on Javascript, so I should be able to do it the same way, right?

Um, kind of. It’s probably doable, but it’s messy. It means lots and lots <script> tags, and it’s ugly.

Another option is always messing in the controllers and getting XML from the blog, and trying to display it somehow – will take way too much time as I’d have to learn everything from the basics. Cost-benefit analysis says no.

What do we do then? There is a thing called JSONP. You can read in detail about it on this page, but basically it’s like saying

<script src="http://example.com/some_script.js"></script>

only different :)

Let’s just show some examples. I have a blog, and I would like to show my posts titles on some other site. My blog responds to this call:

// http://thinksimple.pl/entries/json.js?count=3
([
  {"link": "http://thinksimple.pl/entries/77-Different-submenu-depending-on-controller", 
  "title": "Different submenu depending on controller", 
  "id": 77}, 
  {"link": "http://thinksimple.pl/entries/74-RubyMine-1-1", 
   "title": "RubyMine 1.1", 
   "id": 74}, 
  {"link": "http://thinksimple.pl/entries/72-mysqldump", 
   "title": "mysqldump", 
   "id": 72}
])

(it really does, you can check it!)
That’s cool, but I want to get this data and display it nicely. It would be great if I could create some javascript function in the external app and feed it with the received data. Ok, let’s do it:

<div id="entries"></div>
<script type="text/javascript">
  // jQuery
  $.getJSON(blog_url + '/entries/json.js?count=3&jsoncallback=?', 
        function(data) {
          for (var i in data) {
            $('#entries').append('<p><a href="' 
              + data[i]['link'] + '">' 
              + data[i]['title'] + '</a>'
              + '</p>');
          }
  });
</script>

So I ask for http://thinksimple.pl/entries/json.js?count=3&jsoncallback=jsonp1245158265283 and I get

jsonp1245158265283([
  {"link": "http://thinksimple.pl/entries/77-Different-submenu-depending-on-controller", 
  "title": "Different submenu depending on controller", 
  "id": 77}, 
  {"link": "http://thinksimple.pl/entries/74-RubyMine-1-1", 
   "title": "RubyMine 1.1", 
   "id": 74}, 
  {"link": "http://thinksimple.pl/entries/72-mysqldump", 
   "title": "mysqldump", 
   "id": 72}
])

My blog calls the function that I’ve prepared in my other app! Great, now my problem is solved, no need to dig into an unknown application anytime soon :)

AJAX pagination with Javascript

August 14, 2009

Ryan Bates did an excellent Railscast about pagination with AJAX. I’d like to take it one step further and explain what to do when you want some Javascript functionality in the pagination partial.

Let’s just go with something easy: say you want to highlight clicked products instead of continuing to the show product page. We’ll create a file called highlights.js:

// highlight.js
$(function() {
  function on() { 
    $(this).css('background-color', 'yellow'); 
    return false;
  }

  function off() { 
    $(this).css('background-color', 'transparent'); 
    return false;
  }

  $('.product h3 a').live('click', function() {
    $(this).toggle(on, off);
    return false;
  });
});

Remember to include the file, we can do this in index.html.erb, right below including pagination.js

<% # index.html.erb %>
<% title "Products" %>
<% javascript "pagination" %>
<% javascript 'highlights' %>

<% # more code here... %>

Okay, let’s try it out. First click… Oops, nothing happens. Second click – background turns yellow. Third click – background turns back white. Maybe second page will be better. First click – nothing happens. Second click – there it is, yellow. Third click – white again.

Everything works except for that nasty first click, where did this come from?

Remember when we did this:

$('.product h3 a').live('click', function() {
  $(this).toggle(on, off);
  return false;
});

What this live thing does is bind a click event to all selected elements. The problem is that toggle does exactly the same thing. So when we click for the first time, the toggle method is run for the first time, and therefore it doesn’t toggle anything, it just binds another click event. From then on everything works like it should. The simplest solution would be to call toggle right after live:

$('.product h3 a').live('click', function() {
  $(this).toggle(on, off);
  return false;
});

$('.product h3 a').toggle(on, off);

First page – great, works! Second page – oh crap! Apparently everything that’s in the highlight.js file has been done just once on the initial page load, and that includes the toggle line we have just added. It should be done every time you change the page, because there are new links that need to have the click bound. If so, how about adding the toggle line into index.js.erb?

$("#products").html("<%= escape_javascript(render("products")) %>");
$('.product h3 a').toggle(on, off);

Trying the second page – my Firebug says “on” is not defined. That gives me an idea. Let’s change our highlights.js file to this:

function on() { 
  $(this).css('background-color', 'yellow'); 
  return false;
}

function off() { 
  $(this).css('background-color', 'transparent'); 
  return false;
}
  
$(function() 
{
  $('.product h3 a').live('click', function() {
    $(this).toggle(on, off);
    return false;
  });

  $('.product h3 a').toggle(on, off);
});

The on and off functions are now defined regardless of whether the page is ready or not. Check it again – tadaa!

Ok so just to sum up what we need to do from Ryan’s final code:

And actually, while we’re at it, we might remove the live call altogether. We need to bind the click event on our own each time anyway, so this is not needed anymore. So the final highlights.js looks like this:

function on() { 
  $(this).css('background-color', 'yellow'); 
  return false;
}

function off() { 
  $(this).css('background-color', 'transparent'); 
  return false;
}
  
$(function() 
{
  $('.product h3 a').toggle(on, off);
});

And you know what, I’m just going to say it. AJAX pagination sucks.

Javascript gallery

September 23, 2008

http://ajaxorized.com/projects/image-transition-manager-image-transitions-with-scriptaculous/