Main Content:

Posts tagged as 'javascript'

6 Dual List Boxes 4 comments

May 06

Matthew Ryan asks:

One thing I have been wondering what the best way to approach in Ruby on Rails is Dual List Boxes. You may of seen them on some sites where you can click the arrows in between the two lists and move values from one to the other.

This is a bit complicated to be taken in from a screencast, so I thought a standard post was more appropriate, and whilst this is not directly related to Rails, I think it would be a useful feature within many web applications so lets dive right in.

Analysis

The situation is a need to allow users to select values from a predefined list - so basically this would plainly be a list of checkboxes, so that's where we will start. In Rails, this functionality is what a tagging system would provide, and there are numerous gems and plugins around for that, but they may not be suitable for any system, so we'll build our own using simply has_and_belongs_to_many relationships.

Personally, I am an advocate of so-called unobtrusive JavaScript, which allows you to create websites and applications without requiring JavaScript. Because of this I always start in Rails, creating functionality without any JavaScript, then building on top.

Rails-side

In my application we already have products, so for the example we'll generate categories of which products can be included within, you can add more fields if you want, but just a name and ID will do for now:

script/generate scaffold category name:string

You can now boot up Rails and populate the table with a few categories. Next we need our link table. For has_and_belongs_to_many relationships the table name should be the two current table names in alphabetical order separated by an underscore, so products and categories become "categories_products", so lets create our migration:

script/generate migration create_categories_products

And fill it with our relations:

create_table :categories_products do |t| t.belongs_to :category t.belongs_to :product end

Run the migration, and then we can go into our models and setup the relation with has_and_belongs_to_many followed by the name of the linked model:

In app/models/product.rb:

has_and_belongs_to_many :categories

In app/models/category.rb:

has_and_belongs_to_many :products

So there we have our relationship setup, now for the interesting bit. In our form for creating and editing the products (I suggest it's in a partial of it's own), we need to display the checkboxes.

<div class="dual_control"> <% for category in Category.find(:all, :order => 'categories.name ASC') do %> <div> <%= check_box_tag "product[category_ids][]", category.id, @product.categories.include?(category), :id => "product_categories_#{category.id}" %> <label for="product_categories_<%= category.id %>"><%= category.name %></label> </div> <% end %> </div>

What you are seeing above is a container for each dual control, they are identified by the class, so you can have many per page. We use Rails to loop through each category and output a checkbox and label for each one, we will use the label for the select list. It is important each checkbox has it's own unique ID because JavaScript will use it to make the changes in the background for Rails to process.

Rails will process the checkboxes as an array because the names end with empty square brackets, and it will even setup the linkage automatically, so no additional code is necessary in your model or controller (except for one line below).

The only thing you need to add to complete the functionality is in the controller, because if you deselect all the checkboxes nothing will be sent so add the line below, directly below the start of your update method. This basically forces Rails to empty the associations if no value is passed.

params[:product][:category_ids] ||= []

Now we have our basic system working you can test it in your browser, no JavaScript is needed.

JavaScript niceties

So this is all nicely working, and you could stop there, but from a usability point of view - millions of checkboxes isn't ideal, so dual list boxes is the obvious choice... until you realise how difficult they are to code. Well, to be honest, they aren't difficult, just lengthy.

So to save you all a multiple-day-spanning coding spree, I have written a Prototype based class to simply the solution into one copy-paste action, all you need to do is copy and paste this into your public/javascripts/application.js file:

var DualControl = Class.create({ initialize: function(element, size) { // setup variables if(size == null) size = 5; // local data store this.element = $(element) this.selected_items = new Array(); this.deselected_items = new Array(); // create the new form this.deselected_box = document.createElement('select') this.deselected_box.size = size this.selected_box = document.createElement('select') this.selected_box.size = size this.button_container = document.createElement('div') this.button_container.addClassName('button_container') this.remove_button = document.createElement('button') this.remove_button.innerHTML = '< Remove' this.add_button = document.createElement('button') this.add_button.innerHTML = 'Add >' // loop through the items var items = this.element.childElements() for(var i = 0; i < items.length; i++) { var input = items[i].down('input') var label = items[i].down('label') // if it's checked if(input && input.checked) { // add to selected this.selected_items[this.selected_items.length] = [input.id, label.innerHTML] this.selected_box.options.add(this.create_option(input.id, label.innerHTML)) // if it's not checked } else { // add to deselected this.deselected_items[this.deselected_items.length] = [input.id, label.innerHTML] this.deselected_box.options.add(this.create_option(input.id, label.innerHTML)) } // hide the element - but don't use hide() because // that sets the display to none, which means it won't // be passed to the server. items[i].addClassName('hidden') } // add the new form this.element.appendChild(this.deselected_box) // bind as event listener passes 'this' as this object, // otherwise the event would thing 'this' is the button. this.remove_button.observe('click', this.remove.bindAsEventListener(this)) this.button_container.appendChild(this.remove_button) this.button_container.appendChild(this.add_button) this.element.appendChild(this.button_container) this.add_button.observe('click', this.add.bindAsEventListener(this)) this.element.appendChild(this.selected_box) }, add: function(e) { Event.stop(e) // if an item is selected in the left hand box if(this.deselected_box.selectedIndex >= 0) { // load the item from the array var item = this.deselected_items[this.deselected_box.selectedIndex] // if the item is found if(item) { // check the box $(item[0]).writeAttribute('checked', true) // remove the item from the deselected array this.deselected_items.splice(this.deselected_box.selectedIndex, 1) // remove it from the deselected box (the left one) this.deselected_box.remove(this.deselected_box.selectedIndex) // add the item to the selected array this.selected_items[this.selected_items.length] = item // add it to the selected box (the right one) this.selected_box.options.add(this.create_option(item[0], item[1])) } } }, remove: function(e) { Event.stop(e) // see documentation above // but in reverse if(this.selected_box.selectedIndex >= 0) { var item = this.selected_items[this.selected_box.selectedIndex] if(item) { $(item[0]).writeAttribute('checked', false) this.selected_items.splice(this.selected_box.selectedIndex, 1) this.selected_box.remove(this.selected_box.selectedIndex) this.deselected_items[this.deselected_items.length] = item this.deselected_box.options.add(this.create_option(item[0], item[1])) } } }, create_option: function(value, text) { // just used for quickly creating options // for a select box var optn = document.createElement("option"); optn.text = text; optn.value = value; return optn; } })

I appreciate I haven't really taught you about the JavaScript here, but to analyse 100 lines of code which is all pretty understandable just from looking at it is rather pointless - and I am sure you would appreciate another screencast instead. I've included as many comments as possible for those who wish to understand it.

For our final slice of JavaScript we need to iterate through all the elements with a class of dual_control when the window loads to set them up:

document.observe('dom:loaded', function() { initiate_dual_control() }) var initiate_dual_control = function() { els = $$('.dual_control'); for(var i = 0; i < els.length; i++) new DualControl(els[i], 10); }

Styling

You can customise the style to however it suits your app, but you need to make sure the hidden class is defined to hide without using display:none, as below:

.hidden { width:0; height:0; overflow:hidden; display:block; opacity:0; }

Lastly here is my example styles:

.dual_control { height:100px; } .dual_control select { height:100px; float:left; } .dual_control .button_container { width:80px; height:50px; margin:25px 5px; float:left; } .dual_control .button_container button { width:80px; }

Any problems or queries feel free to let me know in the comments. You can download a zip of my files here.

3 Verified Degradable Deletion 2 comments

April 15

I use scaffold for the basis of all my applications at the moment, it provides a sturdy starting point that you can easily build upon, but there are a few issues that need to be solved before getting started, the first of which is deletion.

The problem with the current method of deletion in scaffolding is it requires upon JavaScript, which you shouldn't really rely upon. While in some cases you can say "my users will definitely have JavaScript enabled" I believe it's best practice to assume otherwise anyway.

You could simply provide a form & button in the show view, but that is not always the best place usability-wise, so I tend to make a separate action just for the form and use JavaScript that will fallback (or degrade) to using it.

Download Screencast: Mirror 1 | Mirror 2 | iPhone Size: 32.0mb, Time: 05:10

1 Automated Client-Side Validation 9 comments

March 31

Having to write separate JavaScript to validate user input before submission of forms can be a right pain, not only is it not very DRY, but it wastes time. Using AJAX you can cheat, harnessing the server-side Rails validation you have already written.

To do this, we use the remote_form_for helper, which uses AJAX to load an RJS result (my my, how many acronyms in this post). The RJS result will be either adding/updating the error list, or redirecting to the new post as normal.

Rails also highlights fields which have errors, so to get this functionality, we can use the visual_effect helper, passing each ID of the field from the errors of the object.

I believe this simple technique has been overlooked in the past, so I hope you find it useful.

Please feel free to leave a comment to let me know of any problems and I'd appreciate any feedback you may have.

Download Screencast: Mirror 1 | Mirror 2 | iPhone Size: 47.5mb, Time: 12:24