Blog
what did i learn today
setting a html5 data attribute with jquery

Abstract

I have a single bootstrap modal, which is called from different places, and so the modal contains some data-* attributes I want to set before showing it. Just using the .data() offered by jquery does not work.

Detailed example

Suppose you have mark-up like this:

<div data-some-important-value="123">

Asking the value is quite easy:

$('[data-some-important-value]').data('some-important-value')

And, according to the documentation, setting the data on a DOM element, should be as easy as

$('[data-some-important-value]').data('some-important-value', 'new-value')

If you would try this in the console, you could verify that does not work. This is where it gets confusing (to me). Apparently the .data() offered by jquery existed before the HTML5 data-* elements did, and they nicely integrated them. But the data-* are only loaded once, and never written back to the document.

To still be able to do this, use the .attr() method instead:

$('[data-some-important-value]').attr('data-some-important-value', 'new-value')

Now I only have to include one modal "template" in my HTML, and set the data-* attributes to customize the behaviour.

rails3 doing cross-browser json

I have two rails applications that communicate together. In the first application i have the following method in my controller: [ruby] def delivery_status envelope = Envelope.find(params[: id]) render : json => envelope.to_json end [/ruby] which, if go to the url in my browser, nicely shows my the JSON. Seems ok. However, if i call this through jQuery, from my second application, using the following: [javascript] $(document).ready(function(){ $('.get_sms_details').live('click', function(event) { var el = $(this), data_url = el.attr('data-url'); $.ajax({ url: data_url, dataType: 'text', success: function(data, status, req) { alert('received with status' + status) alert('received json ' + data); } }); }); }); [/javascript] then the right url is hit with the correct parameters but data is always empty. I first tried using $.getJSON, then $.get to end at $.ajax. I am not sure but it seemed i was doing something wrong at server-side. The request looked fine inside firebug, but the response was always empty. Yet, i did not understand, if let the browser hit the same url, i got my json object. So how do you solve this? Well, i was reading the documentation of $.ajax, and there i found:

When data is retrieved from remote servers (which is only possible using the script or jsonp data types), the operation is performed using a script tag rather than an XMLHttpRequest object.

So, jsonp was the way, but how? First, i changed my jQuery code: [sourcecode language="javascript"] $(document).ready(function(){ $('.get_sms_details').live('click', function(event) { var el = $(this), data_url = el.attr('data-url'); data_url = data_url $.ajax({ url: data_url, dataType: 'jsonp', success: function(data) { envelope = data.envelope; alert('received envelope ' + data.envelope.id); } }); }); }); [/sourcecode] but then my server-side needed to be able to handle the jsonp. I handled that using the following code: [ruby] def delivery_status envelope = Envelope.find(params[:id]) render_json envelope.to_json(:include => [: deliveries, : log_lines]) end private # render json, but also allow JSONP and handle that correctly def render_json(json, options={}) callback, variable = params[:callback], params[:variable] logger.debug("render json or jsonp? Callback = #{callback}, Variable=#{variable}") response = begin if callback &amp;&amp; variable "var #{variable} = #{json};\n#{callback}(#{variable});" elsif variable "var #{variable} = #{json}" elsif callback "#{callback}(#{json});" else json end end render({:content_type => :js, :text => response}.merge(options)) end [/ruby] Where the render_json does all the dirty work for me :) I was somewhat expecting this to be standard inside rails3, and as Kevin Chiu pointed out in the comments, it is and much simpler at that: [ruby] def delivery_status envelope = Envelope.find(params[:id]) render :json => envelope.to_json(:include => [: deliveries, : log_lines]), :callback => params[:callback] end [/ruby] Awesome :)I have two rails applications that communicate together. In the first application i have the following method in my controller: [ruby] def delivery_status envelope = Envelope.find(params[: id]) render : json => envelope.to_json end [/ruby] which, if go to the url in my browser, nicely shows my the JSON. Seems ok. However, if i call this through jQuery, from my second application, using the following: [javascript] $(document).ready(function(){ $('.get_sms_details').live('click', function(event) { var el = $(this), data_url = el.attr('data-url'); $.ajax({ url: data_url, dataType: 'text', success: function(data, status, req) { alert('received with status' + status) alert('received json ' + data); } }); }); }); [/javascript] then the right url is hit with the correct parameters but data is always empty. I first tried using $.getJSON, then $.get to end at $.ajax. I am not sure but it seemed i was doing something wrong at server-side. The request looked fine inside firebug, but the response was always empty. Yet, i did not understand, if let the browser hit the same url, i got my json object. So how do you solve this? Well, i was reading the documentation of $.ajax, and there i found:

When data is retrieved from remote servers (which is only possible using the script or jsonp data types), the operation is performed using a script tag rather than an XMLHttpRequest object.

So, jsonp was the way, but how? First, i changed my jQuery code: [sourcecode language="javascript"] $(document).ready(function(){ $('.get_sms_details').live('click', function(event) { var el = $(this), data_url = el.attr('data-url'); data_url = data_url $.ajax({ url: data_url, dataType: 'jsonp', success: function(data) { envelope = data.envelope; alert('received envelope ' + data.envelope.id); } }); }); }); [/sourcecode] but then my server-side needed to be able to handle the jsonp. I handled that using the following code: [ruby] def delivery_status envelope = Envelope.find(params[:id]) render_json envelope.to_json(:include => [: deliveries, : log_lines]) end private # render json, but also allow JSONP and handle that correctly def render_json(json, options={}) callback, variable = params[:callback], params[:variable] logger.debug("render json or jsonp? Callback = #{callback}, Variable=#{variable}") response = begin if callback && variable "var #{variable} = #{json};\n#{callback}(#{variable});" elsif variable "var #{variable} = #{json};" elsif callback "#{callback}(#{json});" else json end end render({:content_type => :js, :text => response}.merge(options)) end [/ruby] Where the render_json does all the dirty work for me :) I was somewhat expecting this to be standard inside rails3, but apparently it isn't. Are there any better ways to handle this?

Previously, in rails 2.3.8 i used the prototype-helpers link_to_remote and form_remote_for (amongst others). These had the option to add callbacks as follows: [ruby] link_to_remote "Add to cart", :url => { :action => "add", :id => product.id }, :update => { :success => "cart", :failure => "error" } [/ruby] (an example from the documentation). This example would, upon success update the html-element with class "cart", and upon failure the class "error". The possible callbacks were:

  • :loading: Called when the remote document is being loaded with data by the browser.
  • :loaded: Called when the browser has finished loading the remote document.
  • :interactive: Called when the user can interact with the remote document, even though it has not finished loading.
  • :success: Called when the XMLHttpRequest is completed, and the HTTP status code is in the 2XX range.
  • :failure: Called when the XMLHttpRequest is completed, and the HTTP status code is not in the 2XX range.
  • :complete : Called when the XMLHttpRequest is complete (fires after success/failure if they are present). Now the modus operandi has changed, instead we write: [ruby] link_to "Add to cart", :url => {:action => "add", :id => product.id}, :remote => true [/ruby] and it seems there is no option to set the callbacks anymore. Instead of a normal html, we now render javascript, like this (in jquery) : [ruby] $('.cart').replaceWith(<%= escape_javascript(render :partial => 'cart') %>) [/ruby] But how do you handle an error situation? Do i handle it in my controller, and use seperate views? It would seem useful to me to somehow be able to mimic the behaviour we had before. Luckily in this article I was able to find the solution. I had already found that in rails.js the following callbacks were checked:
  • ajax:beforeSend : triggered before executing the AJAX request
  • ajax:success : triggered after a successful AJAX request
  • ajax:complete : triggered after the AJAX request is complete, regardless the status of the response
  • ajax:error : triggered after a failed AJAX request, as opposite to ajax:success But i had no idea how to provide these callbacks. The javascript should be unobtrusive, so this coupling is not done straight in the HTML anymore. From the same article i found a very clear example how to solve this. Take the following Rails 2.3.8 code : [ruby] <% form_remote_tag :url => { :action => 'run' }, :id => "tool-form", :update => { :success => "response", :failure => "error" }, :loading => "$('#loading').toggle()", :complete => "$('#loading').toggle()" %> [/ruby] That translates to this in Rails3 : [ruby] <% form_tag url_for(:action => "run"), :id => "tool-form", :remote => true do %> [/ruby] and inside some javascript (application.js), you bind the events [javascript] jQuery(function($) { // create a convenient toggleLoading function var toggleLoading = function() { $("#loading").toggle() }; $("#tool-form") .bind("ajax:beforeSend", toggleLoading) .bind("ajax:complete", toggleLoading) .bind("ajax:success", function(data, status, xhr) { $("#response").html(status); }); }); [/javascript] For completeness, here is a list of the events and their expected parameters: [javascript] .bind('ajax:beforeSend', function(xhr, settings) {}) .bind('ajax:success', function(data, status, xhr) {}) .bind('ajax:complete', function(xhr, status) {}) .bind('ajax:error', function(xhr, status, error) {}) [/javascript] [UPDATED 7/2/2012] Updated to reflect the new event-names. :loading was renamed to :beforeSend, and :failure was renamed to :error.

I have created a Rails3 application, started with Haml/Sass and finding it awesome. I am also trying to do unobtrusive javascript. Before, in Rails 2.3, I would have expected a remote-form to have an :update attribute, where you could specify a selector where the response of the remote method would be rendered. Now it needs to be done differently: my controller function will render a ".js" view, which will do the necessary actions itself. This makes it more library agnostic, and i prefer jquery. So for instance i have a controller action "search", and i have a corresponding "search.js.erb" as follows: [ruby] $('.grid').replaceWith('<%= escape_javascript(render :partial => 'list') %>') [/ruby] This works. It will replace the html of the element with a class grid with the html from my rendered partial. I started out trying to achieve that in HAML, and failed at first. So first i created the above ERB code which worked. Now my task seemed simpler: translate this seemingly simple line to HAML. My first naive approach was to do the following: [ruby] = "$('.grid').replaceWith('#{escape_javascript(render :partial => 'list')}')" [/ruby] But this just places the html as a readable string inside my page. Even i do something simple like [ruby] = "$('.grid').replaceWith('<h3>Text</h3>') [/ruby] i see the actual characters [ruby] <h3>Text</h3> [/ruby] and not the markup. But i need to contain the javascript inside the double-quotes or haml will not recognise it, and can not interpolate my ruby there. After looking through the HAML reference, actually the solution was incredible simple. Once more. [ruby] != "$('.grid').replaceWith('#{escape_javascript(render :partial => 'list')}')" [/ruby] The != unescapes HTML (as opposed to the standard =). This is exactly what we need of course.

With Rails3 i can create a project fully cut to my needs. I will write it down here, just so i remember it well and hopefully it will help some of you too. In my projects, i want to use haml, rspec2, factory-girl, jquery, ... We need a few steps to complete this.

Create the project

For starters, create the folder, without Test::Unit (-T) and without prototype (-J). [ruby] rails new test-project -T -J [/ruby] You could also specify the database, using option -d, [--database=DATABASE], with the following options: `

mysql oracle postgresql sqlite3 (default) frontbase ibm_db`

Specify needed gems

We also have to specify the gems we need. To do that, we need to edit the Gemfile which is located in the root of your project. My file looks as follows: [ruby] source 'http://rubygems.org' gem 'rails', '3.0.0.rc' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' #gem 'pg' gem 'sqlite3-ruby', :require => 'sqlite3' # Use unicorn as the web server # gem 'unicorn' # Deploy with Capistrano # gem 'capistrano' # To use debugger # gem 'ruby-debug' gem 'rails3-generators' gem "bson_ext" gem "haml" gem "haml-rails" gem "jquery-rails" gem "rcov" # we need this here, see http://blog.davidchelimsky.net/2010/07/11/rspec-rails-2-generators-and-rake-tasks/ group :development, :test do gem "rspec-rails", ">= 2.0.0.beta.18" end # test-environment gems group :test, :spec, :cucumber do gem "factory_girl_rails" gem "rspec", ">= 2.0.0.beta.18" gem "remarkable", ">=4.0.0.alpha2" gem "remarkable_activemodel", ">=4.0.0.alpha2" gem "remarkable_activerecord", ">=4.0.0.alpha2" gem "capybara" gem "cucumber" gem "database_cleaner" gem "cucumber-rails" end [/ruby] Run bundle install to install all needed gems.

Use wanted generators

Then now edit the file application.rb, located in the config folder, and add the following lines: [ruby] # Configure generators values config.generators do |g| g.test_framework :rspec, :fixture => true g.fixture_replacement :factory_girl, :dir=>"spec/factories" end [/ruby] This will make sure that the generators use our own defaults. So now when you type [ruby] rails generate model TestModel name:string rails generate controller TestModel [/ruby] and it will create haml views, rspec tests, and factories. Awesome :)

Prepare to use jquery!

To start using jQuery in Rails3, you have to first get the rails.js specifically for jQuery (stored in the jquery-ujs project). Save it in your public\javascripts directory. Download jquery, and make sure to include both in your application layout or view. In HAML it would look something like. [ruby] = javascript_include_tag 'jquery.min.js' = javascript_include_tag 'rails' [/ruby] BUT: there is a quicker route! We also installed the gem jqeury-rails, which has the following generator if we want to install jquery in one go! [ruby] rails g jquery:install #--ui to enable jQuery UI [/ruby] Rails requires an authenticity token to do form posts back to the server. This helps protect your site against CSRF attacks. In order to handle this requirement the driver looks for two meta tags that must be defined in your page's head. Luckily rails makes it easier for us, again, and we just need to include csrf_meta_tag somewhere inside your page's head (rails3 does this default in your application.html.erb). Be sure to include it in your haml code too. An example application.html.haml would look like this: [ruby] !!! Strict %html{ "xml:lang" => "en", :lang => "en", :xmlns => "http://www.w3.org/1999/xhtml" } %head %title== Test-project #{@page_title} %meta{'http-equiv' => "Content-Type", :content => "text/html; charset=utf-8"} %link{ :href => "/favicon.ico", :rel => "shortcut icon" } = stylesheet_link_tag :all = javascript_include_tag 'jquery-1.4.2.min.js', 'rails.js', 'application.js', :cache => 'all_1' = csrf_meta_tag = yield :local_javascript = yield :head %body #banner =image_tag 'your-logo.png' .the-title %h1 %p Your application #navigation #container #sidebar #contents =yield #footer == © 2010 your company [/ruby]

Done! :)

using content_for jquery on document ready

In my Rails application i use a generic application.rhtml for all my pages. Amongst others, this code contains some jQuery code to manipulate my page [javascript] var $j = jQuery.noConflict() $j(document).ready(function() { // remove all alt-tags $j("[alt]").removeAttr("alt"); // validate all forms $j("form").validate({ errorPlacement: function(error, element) { error.appendTo( element.parent() ); }}); }); [/javascript] The things i do in my document ready callback is simple: remove the alt attribute everywhere, so my tooltips will work on my links containing images, and if there is a form on the page i validate it. Now for one form, i need to add special code, so that when a user fills something in a field, a select-box looses some options. The ideal place to do this, is in the document ready. But not for all pages. How do i solve this in some elegant way, and not use a different layout for that page. I choose to use yield and content_for. Like this. Add the following line : [javascript] var $j = jQuery.noConflict() $j(document).ready(function() { // remove all alt-tags $j("[alt]").removeAttr("alt"); <%= yield :script %> // validate all forms $j("form").validate({ errorPlacement: function(error, element) { error.appendTo( element.parent() ); }}); }); [/javascript] and in my view i write: [ruby] <% content_for :script do %> var allOptions = $j('#mention_thm_classification option').clone(); $j('#mention_thm_reference_local').change(function() { var filter_txt = '.ref-'; if (!$j(this).val()) { filter_txt = filter_txt + 'empty'; } else { filter_txt = filter_txt + 'filled'; }; $j('#mention_thm_classification').html(allOptions.filter(filter_txt)); }); <% end %> [/ruby] So what i actually do is, check whether the value of a field changes. When it contains something a select-list with id mention_thm_classification is changed, and only the options with the correct class are kept. For completeness, my select is built as follows: [html] <select name="mention[thm_classification]" id="mention_thm_classification" > <option value='1' style='background-color: #b7f9e2' class="ref-empty " >Green</option> <option value='2' style='background-color: #ffdebb' class="ref-empty " >Orange</option> <option value='3' style='background-color: #ffbbc6' class="ref-empty ref-filled" selected="selected">Red</option></select> [/html] So i have described a nice way to add extra code to my document ready function without needing a special layout-page. And experienced for the umpteenth time how great jQuery is.

I am using the jQuery validate plugin which is awesome for straightforward form validation. What i especially like is that it offers the user a better user experience, because there is no need for the round-trip to the server. I still need to do the validation on the server-side (rails) as well. I guess there should be a way to extract the validations from the model into the view transparently, but for now i just add a class "required" myself. Now i did bump into a seemingly difficult problem, when the user wanted my to validate to having either one of both fields filled in. Luckily google came up with a beautiful answer quick enough! I needed to adapt it somewhat, because my form-styling places each label-field combination inside a div, so i search for the second surrounding 'div' (containing the group of fields for which only one field is required). [javascript] jQuery.validator.addMethod('required_group', function(val, el) { var $module = $j(el).parent('div').parent('div'); return $module.find('.required_group:filled').length; }, 'Please fill out at least one of these fields'); [/javascript] How i love the jQuery and Rails community. Awesome!