Showing posts with label ruby. Show all posts
Showing posts with label ruby. Show all posts

Wednesday, February 29, 2012

Rails 3 ajax Autocomplete with jQuery and jQuery UI theme

So we want a form element with autocomplete. First thing to do is installing jquery-rails and jquery-ui-themes gems and require in Gemfile.

Then require one of the jQuery UI theme into app/assets/stylesheets/application.css
 *= require_self
 *= require_tree .
 *= require_style
 *= require jquery-ui/ui-lightness
*/

 app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require jquery-ui
//= require_tree .

Our controller responds json object in apps/controllers/accounts_controller.rb
    def search_account
        @accounts=Account.where("LOWER(name) LIKE ?", "%#{params[:name].downcase}%").limit(params[:maxRows]).order(:name)
        
        respond_to do |format|
            format.json {render :json => @accounts}
        end
    end


I've created a helper for input element app/helpers/application_helper.rb
(Take care of heredoc strings.)

    def autocomplete_input(attributes={})
        #*need* object: this form's object : "person"
        #*need* instance: searcheable object : 'account' 
        # instance_key: primary key of this instance : 'account_id'
        # value: original or default value for instance : 'Yahoo Corp.'
        # value_key: original or default value for instance_key: 252
        #*need* ajax_url: remote url for ajax call without format: url_for(:controller=> :accounts, :action=> :search, :id=>'all')
        # ajax_query_additional_params (Array): ["maxRows: 7", "anything :20"]
        #*need* ajax_query_searchable_param Object's string (String): "name" (@person.account.name)
        #*need* ajax_query_searchable_param_key Object's foreign key (String): "id" (@person.account.id)
        # min_length minimal chars to start searching: 2    
        
        if attributes.nil? then return false end
        if attributes.empty? then return false end
        object=attributes[:object]
        instance=attributes[:instance]
        if object.nil? or instance.nil? then return false end    
        object=object.to_s.downcase
        instance=instance.to_s.downcase
        instance_key=attributes[:instance_key] || instance + "_id"
        value=attributes[:value] || ""
        value_key=attributes[:value_key] || ""
        value_key_html=""
        unless value_key.to_s.empty? then
            value_key_html=" value=\""+value_key.to_s+"\"" 
        end
        ajax_url=attributes[:ajax_url]
        ajax_query_additional_params=attributes[:ajax_query_additional_params] || ""
        ajax_query_searchable_param=attributes[:ajax_query_searchable_param] || "name"
        ajax_query_searchable_param_key=attributes[:ajax_query_searchable_param_key] || "id"
        min_length=attributes[:min_length] || 2
        
        ajax_query_additional_params_formatted=""
        unless ajax_query_additional_params.nil? then
            case ajax_query_additional_params
                when Array then
                    i=0
                    ajax_query_additional_params.each do |aqap|
                        if i==0 then
                            ajax_query_additional_params_formatted=ajax_query_additional_params_formatted + aqap.to_s
                        else
                            ajax_query_additional_params_formatted=ajax_query_additional_params_formatted + ",\n" + aqap.to_s
                        end
                        i=i.next
                    end
                when String then
                    ajax_query_additional_params_formatted=ajax_query_additional_params
                when Hash then
                    i=0
                    ajax_query_additional_params.each do |aqap_key,aqap_value|
                        if i==0 then
                            ajax_query_additional_params_formatted=ajax_query_additional_params_formatted + aqap_key.to_s + ": " + aqap_value.to_s
                        else
                            ajax_query_additional_params_formatted=ajax_query_additional_params_formatted + ",\n" + aqap_key.to_s + ": " + aqap_value.to_s
                        end
                        i=i.next
                    end
            end
        end
        jquery_request_data_params="data: {\n"
        unless ajax_query_additional_params_formatted.empty? then
            jquery_request_data_params=jquery_request_data_params + ajax_query_additional_params_formatted + ",\n"
        end
        jquery_request_data_params=jquery_request_data_params + "#{ajax_query_searchable_param}: request.term\n},"
        search_field_id="search_#{object}_#{instance}"
        value_div_id="#{object}_#{instance}_log"
        hidden_field_id="#{object}_#{instance_key}"
        hidden_field_name="#{object}[#{instance_key}]"
        function_log_name="log_#{object}_#{instance}"
        
        html_text = <<HTML1
<table><tbody><tr>
    <td><input id="#{search_field_id}" class="ui-autocomplete-input"/></td>
    <td><div id="#{value_div_id}" class="ui-widget-content">#{value}</div></td>
</tr></tbody></table>
<input type="hidden" id="#{hidden_field_id}" name="#{hidden_field_name}" #{value_key_html} >

HTML1
        
         js_text = <<JS1
        
$(function() {

    function #{function_log_name}( label, id ) {
        $( "##{value_div_id}" ).html(label);
        $( "##{hidden_field_id}").val(id);
    }

    $( "##{search_field_id}" ).autocomplete({
        source: function( request, response ) {
            $.ajax({
                url: "#{ajax_url}.json",
                dataType: "json",
                #{jquery_request_data_params}
                success: function( data ) {
                    response( $.map( data, function( item ) {
                        return {
                            label: item.#{ajax_query_searchable_param},
                            value: item.#{ajax_query_searchable_param},
                            id: item.#{ajax_query_searchable_param_key}
                        }
                    }));
                }
            });
        },
        minLength: #{min_length},
        select: function( event, ui ) {
            if (ui.item) {
                #{function_log_name}( ui.item.value, ui.item.id );
            } else {
                #{function_log_name}( this.value, this.value );
            }
        },
        open: function() {
            $( this ).removeClass( "ui-corner-all" ).addClass( "ui-corner-top" );
        },
        close: function() {
            $( this ).removeClass( "ui-corner-top" ).addClass( "ui-corner-all" );
        }
    });
});
        
JS1
    concat(raw(javascript_tag(js_text)))
    concat(raw(html_text))
    end

Finally insert autocomplete element in _form.htm.erb

<div class="ui-widget">
<label for="search_account">Account:</label><br />
<% autocomplete_input(:object    => "person",
            :instance            => "account",
            :instance_key        => "account_id",
            :value               => (@person.account.name unless @person.account.nil?),
            :value_key           => @person.account_id,
            :ajax_url            => url_for(:controller => 'accounts',
                                            :action     => 'search_account',
                                            :id         => 'all'),
            :ajax_query_additional_params    => {:maxRows => 7},
            :ajax_query_searchable_param     => "name",
            :ajax_query_searchable_param_key => "id",
            :min_length          => 2
)%>
</div>
</div>

Works with Rails 3.2 and jQuery JavaScript Library v1.7.1. Good luck! Questions?

Tuesday, August 9, 2011

How to create scriptaculous ajax inplace editor fields in Rails3 with validation

First, we should do our html view in app/views/ips/index.html:

<div id="ips_table_div">
<%= render "ips_table" %>
</div>

Then, we create the table and the javascript code in our partial view (app/views/ips/_ips_table.html.erb).

<table cellpadding="10"><tbody>

<% unless @ips.nil? %>

		<tr>

			<td id="ip_new_ip_td_div">
				<%= form_tag url_for(:controller => :ips,
							:action	=> :create_ip),
						:remote => true,
						:id 	=> "new_ip" %>
					
						<%= text_field_tag "ip" %>
						<%= submit_tag t(:add_button, :scope => [:application]), :id => "new_ip_submit" %>
				</form>
			</td>

			<td>
			</td>

		</tr>

	<% @ips.each do |ip| %>
		<tr>

			<td id="ip_ip_td_div_<%=ip.id %>">
				<div id="ip_ip_text_div_<%=ip.id %>"><%= ip.ip %></div>
			</td>

			<td>
				<%= link_to t(:delete_link, :scope => [:application]),
 								url_for(:controller => :ips,
									:action	=> :delete_ip,
									:id 	=> ip.id),
								:remote => true,
								:confirm => t(:ip_delete_confirm, :scope => [:application])%>
			</td>

		</tr>
	<% end %>

<% end %>

</tbody></table>

<script type="text/javascript">
var ip_validate_background_color_error = "#fff999";
var ip_validate_border_color_error = "#f80000";
var ip_validate_background_color = "#ffffff";
var ip_validate_border_color = "#999999";

var ipModifyFormOptions= $H({
		cancelText:'<%=t :cancel_link, :scope => [:application] %>',
		okText: 'ok',
		savingText: '<%=t :saving_text, :scope => [:application] %>',
		loadingText: '<%=t :loading_text, :scope => [:application] %>',
		clickToEditText: '<%=t :click_to_edit_text, :scope => [:application] %>',
		
});

<% unless @ips.nil? %>

Form.findFirstElement('new_ip').style.borderColor = ip_validate_border_color;
Form.findFirstElement('new_ip').style.backgroundColor = ip_validate_background_color;
Form.findFirstElement('new_ip').setAttribute("onkeypress","return input_filter_exact(event, 'ip')");
$('new_ip_submit').disabled=true;
new Form.Observer('new_ip', 0.3, function(form, value) {
		if(validateIpAddress(Form.findFirstElement('new_ip').value, false) == true) {
				Form.findFirstElement('new_ip').style.borderColor = ip_validate_border_color;
				Form.findFirstElement('new_ip').style.backgroundColor = ip_validate_background_color;
				$('new_ip_submit').disabled=false;
		}	else {
				Form.findFirstElement('new_ip').style.borderColor = ip_validate_border_color_error;
				Form.findFirstElement('new_ip').style.backgroundColor = ip_validate_background_color_error;
				$('new_ip_submit').disabled=true;
		}
});


	<% @ips.each do |ip| %>

new Ajax.InPlaceEditor('ip_ip_text_div_<%=ip.id %>', '<%= url_for :controller => :ips, :action => :modify_ip, :id => ip.id	%>', ipModifyFormOptions.merge({
	formId: 'ip_ip_form_<%=ip.id %>',
	onEnterEditMode: function(form, value) {
			setTimeout(function() {
					Form.findFirstElement('ip_ip_form_<%=ip.id %>').style.borderColor = ip_validate_border_color;
					Form.findFirstElement('ip_ip_form_<%=ip.id %>').style.backgroundColor = ip_validate_background_color;
					Form.findFirstElement('ip_ip_form_<%=ip.id %>').setAttribute("onkeypress","return input_filter_exact(event, 'ip')");
					new Form.Observer('ip_ip_form_<%=ip.id %>', 0.3, function(form, value) {
						if(validateIpAddress(Form.findFirstElement('ip_ip_form_<%=ip.id %>').value, false) == true) {
							Form.findFirstElement('ip_ip_form_<%=ip.id %>').style.borderColor = ip_validate_border_color;
							Form.findFirstElement('ip_ip_form_<%=ip.id %>').style.backgroundColor = ip_validate_background_color;
							$('ip_ip_form_<%=ip.id %>').elements[1].disabled=false;
						}	else {
							Form.findFirstElement('ip_ip_form_<%=ip.id %>').style.borderColor = ip_validate_border_color_error;
							Form.findFirstElement('ip_ip_form_<%=ip.id %>').style.backgroundColor = ip_validate_background_color_error;
							$('ip_ip_form_<%=ip.id %>').elements[1].disabled=true;
						}
				});
			}, 1000);
	}
}).toObject());

	<% end %>

<% end %>
</script>

Then we should make the controller (app/controllers/ips_controller.rb):

respond_to :js, :only => [:delete_ip, :create_ip]
respond_to :html, :except => [:delete_ip, :create_ip]
layout :application, :except => [:delete_ip, :create:ip, :modify_ip]
layout false, :only => [:delete_ip, :create:ip, :modify_ip]

def index
  @ips=Ip.all
  respond_with(@ips)
end

def delete_ip
  Ip.destroy(params[:id])
  # it will returns a javascript code by default, so have to do the
  # app/views/ips/delete_ip.js.erb, what will loads the table completely
 @ips=Ip.all
  respond_with(@ips)
end

def create_ip
  ip=Ip.new(params[:ip]).save
  # it will returns a javascript code, so have to do the
  # app/views/ips/create_ip.js.erb, what will loads the table completely
  @ips=Ip.all
  respond_with(@ips)
end

def modify_ip
  ip=Ip.find(params[:id])
  ip.update_attribute(:ip, params[:value])
  # don't forget some validations here also
  # it will returns a html code, so have to do the
  # app/views/ips/modify_ip.html.erb, what will contains the new text only, or:
  respond_to do |format|
    format.html { render :text => ip.ip }
  end
end

Then we can create returned javascript codes app/views/ips/create_ip.js.erb and delete_ip.js.erb:

  $("ips_table_div").innerHTML='';
  $("ips_table_div").update("<%=escape_javascript(render(:template => "/ips/_ips_table.html.erb", :locals => {:ips => @ips})) %>"); 

Finally we must define javascipt validation functions in public/apllication.js:

function validateIpAddress(value, can_be_empty) {
    if (arguments.length == 1) {
        can_be_empty = false;
    }
    if (can_be_empty == true) {
        if (value == null) {
            return false;
        }
        if (value == "") {
            return true;
        }
    }
    if (value.match(/\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/))
    {
        if (value.length>0 && value.length<256)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    else
    {
        return false; 
    }
}

function input_filter_exact(e, type) {
    var enabled_chars;    
    switch (type)
        {
            case "ip": enabled_chars="0123456789.";
                                    break;
            default: enabled_chars="-/.,_?[]()!abcdefghijklmnopqrstvuwxyzABCDEFGHIJKLMNOPQRSTVUWXYZ0123456789éáőúűóüöíÉÁŐÚÓÜÖÍ ";
        }
    var key;
  var keychar;
    if (window.event)
        key = window.event.keyCode;
  else if (e)
        key = e.which;
    else
        return true;
    keychar = String.fromCharCode(key);
    if ((key==null) || (key==0) || (key==8) || (key==9) || (key==13) || (key==27) )
        return true;
    else if (((enabled_chars).indexOf(keychar) > -1))
        return true;
    else
    return false;
} 


That's all. Works with Rails 3.0.9, Prototype 1.7 and script.aculo.us v1.8.3