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.

keeping observe_field and spinner in sync
I am using a lot of observe_fields in my views. I have two searches on my form, one with two fields one with four, and every time a user types something the observe_field is triggered and some search is executed via AJAX. To make sure the user knows what is going on, i use a spinner-image. [ruby] <%= observe_field 'querypeople', :frequency => 1, :update => 'people_to_link', :before => "Element.show('spinner2')", :loaded => "Element.hide('spinner2')", :complete => "Element.hide('spinner2')", :url => {:action => 'listpeople'}, :with => " 'querypeople=' +escape($('querypeople').value) + '&queryorganisation=' + escape($('queryorganisation').value)" %> <%= observe_field 'queryorganisation', :frequency => 1, :update => 'people_to_link', :before => "Element.show('spinner2')", :loaded => "Element.hide('spinner2')", :complete => "Element.hide('spinner2')", :url => {:action => 'listpeople'}, :with => " 'querypeople=' +escape($('querypeople').value) + '&queryorganisation=' + escape($('queryorganisation').value)" %> [/ruby] This works all fine and dandy, but when a user types multiple characters in a row, the spinner is hidden while another query is running. So i needed to improve that. I first introduced a little code to count how many times a certain spinner is shown: [ruby] <% javascript_tag do %> var spinner_counter=[0,0,0]; function show_spinner(itm) { spinner = (itm == 0) ? "spinner2" : "spinner" ; Element.show(spinner); spinner_counter[itm] = spinner_counter[itm] + 1; } function hide_spinner(itm) { spinner = (itm == 0) ? "spinner2" : "spinner" ; spinner_counter[itm] = spinner_counter[itm] - 1; if (spinner_counter[itm] <= 0) { Element.hide(spinner); spinner_counter[itm] = 0; } } <% end %> [/ruby] and then adapted the ruby-code as follows: [ruby] <%= observe_field 'querypeople', :frequency => 1, :update => 'people_to_link', :before => "show_spinner(0)", :complete => "hide_spinner(0)", :url => {:action => 'listpeople'}, :with => " 'querypeople=' +escape($('querypeople').value) + '&queryorganisation=' + escape($('queryorganisation').value)" %> <%= observe_field 'queryorganisation', :frequency => 1, :update => 'people_to_link', :before => "show_spinner(0)", :complete => "hide_spinner(0)", :url => {:action => 'listpeople'}, :with => " 'querypeople=' +escape($('querypeople').value) + '&queryorganisation=' + escape($('queryorganisation').value)" %> [/ruby] Do you know a better solution?
observe_field in IE8: endless loop
My users showed me the weirdest problem today. In my application is a select-box that allows users to select the classification. When the classification is changed, the div containing the icons for the actions (and the classification-select-box itself) is updated to reflect the new classification. So i use an observe_field to watch the select-box, and trigger some javascript to perform the magic. In Firefox this works perfectly. But my users tend to use IE8, for some obscure reason, and switching to FF or Chrome is ... difficult (What other browser?). The observe_field is written as follows: [ruby] <%= observe_field "mention_thm_classification", :frequency => 0.5, :update => "mention-actions", :with => "classification_id", :before => "Element.show('spinner_cl')", :loaded => "Element.hide('spinner_cl')", :complete => "Element.hide('spinner_cl')", :url => { :action => "set_classification", :id => mention } %> [/ruby] Pretty straightforward. The observe_field is contained inside the div that is updated. When in IE8 a new value is chosen, i get the correct value in my method first, and then i get a whole bunch of classification_id="null". I am guessing what happens is that when the html is updated, somehow also the select-box is emptied, and thus changed, and thus a new value is triggered. So i replace the html again, and the select-box is emptied again, and an endless loop is born. So, the easy solution should be: do not do the action of the observe_field if the value of the select-box is null, and that is actually really easy: [ruby highlight="3"] <%= observe_field "mention_thm_classification", :frequency => 0.5, :update => "mention-actions", :with => "classification_id", :condition => "value != null", :before => "Element.show('spinner_cl')", :loaded => "Element.hide('spinner_cl')", :complete => "Element.hide('spinner_cl')", :url => { :action => "set_classification", :id => mention } %> [/ruby] We add a :condition that checks just that! It still feels i am doing something wrong, that i should not have to do this, so i would be happy to hear any way to improve this.
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!