Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ajax nested form and ajax upload in rails

ajax nested form and ajax upload in rails

explain and demonstrate how to implement ajax nested form and ajax upload in rails project

tsechingho

August 21, 2012
Tweet

More Decks by tsechingho

Other Decks in Programming

Transcript

  1. Ajax nested form &
    Ajax upload in Rails
    何澤清 Tse-Ching Ho
    2012/08/21

    View full-size slide

  2. About
    • https://github.com/tsechingho
    • https://twitter.com/tsechingho
    • https://facebook.com/tsechingho

    View full-size slide

  3. Demo Code
    https://github.com/tsechingho/ajax-tutorial

    View full-size slide

  4. Prerequisite Work

    View full-size slide

  5. Gems supposed to
    be well understood
    • jquery-rails
    • anjlab-bootstrap-rails
    • simple_form
    • carrierwave
    • mini_magick / rmagick

    View full-size slide

  6. Ground rails project
    • Have two models associated with has_many
    • Have one model mounted with carrierwave’s
    uploader
    • Render 'form' in views of new and edit
    • Use respond_with for each action of controller
    • Layout with twitter bootstrap

    View full-size slide

  7. Twitter Bootstrap Modal

    View full-size slide

  8. Handle Feedback
    of .ajaxSubmit() via Modal
    modal
    .feedback
    modal modal
    Error
    feedback
    modal
    modal
    Success
    feedback

    View full-size slide

  9. Creature & CreaturePhoto
    class Creature < ActiveRecord::Base
    attr_accessible :characteristic, :place_of_origin,
    :popular_name, :scientific_name, :animal_handbook_ids
    validates :popular_name, presence: true
    has_many :animal_handbook_creatures
    has_many :animal_handbooks,
    through: :animal_handbook_creatures
    has_many :photos, class_name: 'CreaturePhoto'
    def name
    popular_name
    end
    end
    require 'file_size_validator'
    class CreaturePhoto < ActiveRecord::Base
    attr_accessible :content_type, :file_name, :file_size,
    :creature_id, :source, :source_cache, :remove_source
    validates :source,
    file_size: {
    maximum: 3.megabytes.to_i
    }
    mount_uploader :source,
    CreaturePhotoUploader, mount_on: :file_name
    delegate :url, :current_path, :size, :filename, to: :source
    belongs_to :creature
    before_save :update_attributes_with_source
    end

    View full-size slide

  10. creatures_controller.rb
    class CreaturesController < ApplicationController
    before_filter :load_creature, only: [:show, :edit, :update, :destroy]
    respond_to :html
    def edit
    render 'edit_modal', layout: false if request.xhr?
    end
    def update
    @creature.update_attributes params[:creature]
    if @creature.valid?
    flash[:notice] = 'Creature was successfully updated.'
    end
    respond_with @creature do |format|
    format.html {
    if @creature.valid?
    load_creatures
    render partial: 'table', locals: { creatures: @creatures }
    else
    render 'edit_modal', layout: false
    end
    } if request.xhr?
    end
    flash.discard :notice if request.xhr?
    end
    end

    View full-size slide

  11. twitter_bootstrap_helper.rb
    def iconed_link_to(text, url, options = {})
    icon_class = options.delete(:icon_class)
    link_to url, options do
    content_tag(:i, nil, class: icon_class) << ' ' << text
    end
    end
    def link_to_edit(url, options = {})
    icon_class = options.delete(:icon_class) || 'icon-edit'
    default_options = { title: t('helpers.edit'), class: 'btn', icon_class: icon_class }
    iconed_link_to nil, url, default_options.deep_merge(options)
    end
    def link_to_edit_modal(url, modal_id)
    default_options = { remote: true, data: { target: modal_id, toggle: 'modal', type:
    'html' }, class: 'btn modal-open' }
    link_to_edit url, default_options
    end

    View full-size slide

  12. creatures/index.html.erb


    <%= t('.title') %>

    <%= render_list class: 'nav nav-tabs' do |li|
    li << [link_to_open_modal(t('helpers.new'), new_creature_path, '#creature-
    modal'), { class: 'action' }]
    end %>
    <%= render 'table', creatures: @creatures %>




    View full-size slide

  13. creatures/_table.html.erb


    <%= Creature.human_attribute_name :popular_name %>
    <%= Creature.human_attribute_name :scientific_name %>
    <%= Creature.human_attribute_name :place_of_origin %>
    <%= Creature.human_attribute_name :characteristic %>
    <%= t('helpers.actions') %>

    <% creatures.each do |creature| %>

    <%= creature.popular_name %>
    <%= creature.scientific_name %>
    <%= creature.place_of_origin %>
    <%= creature.characteristic %>

    <%= link_to_show creature_path(creature) %>
    <%= link_to_edit_modal edit_creature_path(creature), '#creature-modal' %>
    <%= link_to_destroy creature_path(creature) %>


    <% end %>

    View full-size slide

  14. creatures/edit_modal.html.erb
    <%= simple_form_for @creature, remote: true, html: { data: { type: 'html' }, class:
    'form-horizontal' } do |f| %>

    ×
    <%= t('.title') %>


    <%= render 'form', f: f %>


    <%= t('helpers.close') %>
    <%= f.button :submit, name: nil, class: 'btn-primary' %>

    <% end %>

    View full-size slide

  15. modal.js.coffee
    $ ->
    $.modal ||= {}
    $.modal.appendFeedback = (modal, data) ->
    $('').addClass('feedback hide').html(data).appendTo(modal)
    $.modal.replaceFeedback = (modal) ->
    modal.html(modal.children('.feedback').html())
    $.modal.enableChosen()
    $.modal.replaceTable = (table_id, modal = $(this)) ->
    feedback_table = modal.find('.table')
    table = $(table_id).find('.table')
    table.html(feedback_table.html())
    modal.find('.feedback').remove().end()
    .modal('hide')
    table.effect('shake')
    return true

    View full-size slide

  16. creatures.js.coffee
    $ ->
    $('#creature-list')
    .on 'ajax:success', 'a.modal-open', (event, data, status, xhr) ->
    modal = $($(this).attr('data-target'))
    modal.html(data)
    $.modal.enableChosen()
    .on 'ajax:error', '.a.modal-open', (event, xhr, status, error) ->
    modal = $($(this).attr('data-target'))
    $.modal.showErrorModal(status, error, modal)
    $('#creature-modal')
    .on 'ajax:success', '.simple_form', (event, data, status, xhr) ->
    modal = $(this).parent()
    $.modal.appendFeedback(modal, data)
    if modal.find('.feedback .alert-error').size() > 0
    $.modal.replaceFeedback(modal)
    return true
    table_id = '#creature-list'
    $.modal.replaceTable(table_id, modal)
    .on 'ajax:error', '.simple_form', (event, xhr, status, error) ->
    modal = $('#creature-modal')
    $.modal.showErrorModal(status, error, modal)

    View full-size slide

  17. application.js
    //= require jquery
    //= require jquery-ui
    //= require jquery_ujs
    //= require bootstrap
    //= require chosen-jquery
    //= require modal
    //= require_tree .

    View full-size slide

  18. Key Points
    • Use respond_with
    • Render 'modal' specific files
    • Render partial files
    • Via data attributes
    • Define rails ajax callbacks
    • Use namespace for javascript methods
    • Catch ajax callback in div container if data-type
    is :html

    View full-size slide

  19. Ajax Nested Form

    View full-size slide

  20. How To
    • Concepts
    • Save template in data attributes
    • DOM manipulation
    • https://github.com/nathanvda/cocoon
    • gem 'cocoon'

    View full-size slide

  21. creature.rb
    class Creature < ActiveRecord::Base
    attr_accessible :characteristic, :place_of_origin, :popular_name, :scientific_name,
    :animal_handbook_ids, :photos_attributes
    validates :popular_name, presence: true
    has_many :animal_handbook_creatures
    has_many :animal_handbooks, through: :animal_handbook_creatures
    has_many :photos, class_name: 'CreaturePhoto'
    accepts_nested_attributes_for :photos, allow_destroy: true, reject_if: proc
    { |obj| obj.blank? }
    def name
    popular_name
    end
    end

    View full-size slide

  22. twitter_bootstrap_helper.rb
    module TwitterBootstrapHelper
    def iconed_link_to_add_association(text, *args)
    args << {} if args.size < 2
    icon_class = args.last.delete(:icon_class) || 'icon-plus'
    default_options = { title: t('helpers.add'), class: 'btn' }
    args.last.deep_merge! default_options
    link_to_add_association *args do
    content_tag(:i, nil, class: icon_class) << ' ' << text
    end
    end
    def iconed_link_to_remove_association(text, *args)
    args << {} if args.size < 2
    icon_class = args.last.delete(:icon_class) || 'icon-remove'
    default_options = { title: t('helpers.remove'), class: 'btn' }
    args.last.deep_merge! default_options
    link_to_remove_association *args do
    content_tag(:i, nil, class: icon_class) << ' ' << text
    end
    end
    end

    View full-size slide

  23. creatures/_form.html.erb
    <%= f.error_notification %>

    <%= f.input :popular_name %>
    <%= f.input :scientific_name %>
    <%= f.input :place_of_origin %>
    <%= f.input :characteristic, input_html: { size: '20x5' } %>
    <%= f.association :animal_handbooks, input_html: { class: 'chzn-select' } %>

    <%= t('.creature_photos') %>

    <%= render 'creature_photos/field_labels', creature_form: f %>
    <%= f.simple_fields_for :photos do |f2| %>
    <%= render 'creature_photos/fields', f: f2 %>
    <% end %>

    <%= iconed_link_to_add_association t('helpers.add'),
    f,
    :photos,
    data: {
    :'association-insertion-node' => '.form-inputs.form-inline',
    :'association-insertion-method' => :append
    },
    partial: 'creature_photos/fields',
    render_options: {
    locals: { }
    } %>

    View full-size slide

  24. creatures/_fields.html.erb
    <%= field_set_tag nil, class: 'creature-fields row-fluid nested-form-hidden-label nested-fields' do %>
    <%= f.input :popular_name, wrapper_html: { class: 'span2' }, input_html: { class: 'span12' } %>
    <%= f.input :scientific_name, wrapper_html: { class: 'span2' }, input_html: { class: 'span12' } %>
    <%= f.input :place_of_origin, wrapper_html: { class: 'span2' }, input_html: { class: 'span12' } %>
    <%= f.input :characteristic, as: :string, wrapper_html: { class: 'span4' }, input_html: { class: 'span12' } %>

    <%= iconed_link_to_remove_association nil, f %>

    <% end %>

    View full-size slide

  25. application.js
    //= require jquery
    //= require jquery-ui
    //= require jquery_ujs
    //= require bootstrap
    //= require chosen-jquery
    //= require cocoon
    //= require modal
    //= require_tree .

    View full-size slide

  26. Key Points
    • accepts_nested_attributes_for :photos, allow_destroy:
    true
    • attr_accessible :photos_attributes
    • Render partial file
    • Use link_to_add_association helper
    • Use link_to_remove_association helper
    • Add 'nested-fields' class to container tag of nested item
    • Require cocoon javascript

    View full-size slide

  27. Ajax Upload
    It’s impossible
    since browsers forbid for security reason.
    It’s possible if we cheat browsers.

    View full-size slide

  28. How To
    • Concepts
    • iFrame Transport
    • rack middleware to modify request header
    • https://github.com/leppert/remotipart
    • gem 'remotipart'

    View full-size slide

  29. http://www.alfajango.com/blog/ajax-file-uploads-with-the-iframe-method/
    iFrame Transport

    View full-size slide

  30. application.js
    //= require jquery
    //= require jquery-ui
    //= require jquery_ujs
    //= require bootstrap
    //= require chosen-jquery
    //= require cocoon
    // Since XMLHttpRequest (AJAX) standard has no support for file uploads,
    // use iframe-transport method of remotipart gem for ajax file upload.
    //= require jquery.remotipart
    //= require modal
    //= require_tree .

    View full-size slide

  31. Other ways?
    • iFrame
    • https://github.com/blueimp/jQuery-File-Upload
    • Flash
    • http://www.uploadify.com
    • Form Data
    • http://hacks.mozilla.org/2010/07/firefox-4-
    formdata-and-the-new-file-url-object/

    View full-size slide

  32. References
    • http://www.alfajango.com/blog/rails-3-remote-links-
    and-forms/
    • http://www.alfajango.com/blog/rails-3-remote-links-
    and-forms-data-type-with-jquery/
    • http://railscasts.com/episodes/196-nested-model-
    form-revised
    • http://www.alfajango.com/blog/ajax-file-uploads-with-
    the-iframe-method/
    • http://os.alfajango.com/remotipart/

    View full-size slide