From 93e258f9fa52af2505e06595772831c884cefe86 Mon Sep 17 00:00:00 2001 From: Kir Shatrov Date: Tue, 2 Dec 2014 12:56:01 -0500 Subject: [PATCH] Reworked client code and JS API --- README.md | 14 +- app/assets/javascripts/activeform.js | 136 ++++++++++++++++++ app/assets/javascripts/link_helpers.js | 61 -------- lib/active_form.rb | 2 +- lib/active_form/view_helpers.rb | 25 ++-- .../generators/form/form_install_generator.rb | 2 +- .../app/assets/javascripts/application.js | 2 +- test/dummy/app/assets/javascripts/projects.js | 19 +-- .../app/views/assignments/_form.html.erb | 17 ++- .../views/assignments/_task_fields.html.erb | 2 +- .../app/views/conferences/_speaker.html.erb | 9 +- test/dummy/app/views/projects/_form.html.erb | 38 +++-- .../projects/_project_tag_fields.html.erb | 5 +- .../app/views/projects/_task_fields.html.erb | 25 ++-- 14 files changed, 228 insertions(+), 129 deletions(-) create mode 100644 app/assets/javascripts/activeform.js delete mode 100644 app/assets/javascripts/link_helpers.js diff --git a/README.md b/README.md index 958963d..668f044 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ Active Form comes with two helpers to deal with this functionality: 1. `link_to_add_association` will display a link that renders fields to create a new object. 2. `link_to_remove_association` will display a link to remove a existing/dynamic object. -In order to use it you have to insert this line: `//= require link_helpers` to your `app/assets/javascript/application.js` file. +In order to use it you have to insert this line: `//= require activeform` to your `app/assets/javascript/application.js` file. In our `ConferenceForm` we can dynamically create/remove `Speaker` objects. To do that we would write in the `app/views/conferences/_form.html.erb` partial: @@ -271,12 +271,16 @@ In our `ConferenceForm` we can dynamically create/remove `Speaker` objects. To d

Speaker Details

- <%= f.fields_for :speakers do |speaker_fields| %> - <%= render "speaker_fields", :f => speaker_fields %> - <% end %> + + <%= render_association_template f, :speakers %> +
+ <%= f.fields_for :speakers do |speaker_fields| %> + <%= render "speaker_fields", :f => speaker_fields %> + <% end %> +
diff --git a/app/assets/javascripts/activeform.js b/app/assets/javascripts/activeform.js new file mode 100644 index 0000000..3f476e6 --- /dev/null +++ b/app/assets/javascripts/activeform.js @@ -0,0 +1,136 @@ +(function($) { + // Form API, draft version: + // $(".task_list").addFormField("tasks") + // $(".task_list").removeFormField("tasks", 2) // destroy second field + + var activeFormHelpers = { + generateResourceId: function() { + return new Date().getTime(); + } + }; + + $.fn.removeFormField = function(nodeToDeleteOrIndex, options) { + if(options == undefined) { + options = {} + } + + var nodeToDelete; + + if(nodeToDeleteOrIndex instanceof jQuery) { + nodeToDelete = nodeToDeleteOrIndex; + } else { + var wrapperClass = this.data('wrapper-class') || 'nested-fields'; + console.log(wrapperClass) + nodeToDelete = this.find("." + wrapperClass).filter(function(i, b) { + return $(b).css('display') != 'none'; + }).eq(nodeToDeleteOrIndex); + } + + this.trigger('before-remove', [nodeToDelete]); + + var timeout = this.data('remove-timeout') || 0; + + var input = nodeToDelete.find("input").filter(function(i, t) { + return false + }) + + var isDynamic = nodeToDelete.hasClass("dynamic"); + + var context = this; + setTimeout(function() { + if (isDynamic) { + nodeToDelete.remove(); + } else { + nodeToDelete.find("input[type=hidden]").val("1"); + nodeToDelete.hide(); + } + context.trigger('after-remove', [nodeToDelete]); + }, timeout); + + return this + }; + + $.fn.addFormField = function(assoc, options) { + if(options == undefined) { + options = {} + } + var templateSelector = "." + assoc + "_template"; + + var templateContainer; + $(this).parents().each(function(i, el){ + var found = $(el).find(templateSelector) + if(found.length > 0) { + templateContainer = found.eq(0) + return false; + } + }) + + var newId = activeFormHelpers.generateResourceId(); + var regex = new RegExp("new_" + assoc, "g"); + var content = templateContainer.html().replace(regex, newId); + var contentNode = $(content); + + contentNode.addClass("dynamic") + + if(!options.insertionMethod) { + options.insertionMethod = 'append'; + } + + this.trigger('before-insert', [contentNode]); + + var addedContent = this[options.insertionMethod](contentNode); + + this.trigger('after-insert', [contentNode]); + + return this; + }; + + $(document).on('click', '.add_fields', function(e) { + e.preventDefault(); + + var $link = $(this); + var form = $link.parents("form").eq(0); + var assoc = $link.data('association'); + var insertionMethod = $link.data('association-insertion-method') || $link.data('association-insertion-position'); + var insertionNode = $link.data('association-insertion-node'); + var insertionTraversal = $link.data('association-insertion-traversal'); + + if (insertionNode){ + if (insertionTraversal){ + insertionNode = $link[insertionTraversal](insertionNode); + } else { + if(insertionNode == "this") { + insertionNode = $(this) + } else { + $(this).parents().each(function(i, el){ + var found = $(el).find(insertionNode) + if(found.length > 0) { + insertionNode = found.eq(0) + return false; + } + }) + } + } + } else { + insertionNode = $link.parent(); + } + + insertionNode.addFormField(assoc, { + insertionMethod: insertionMethod, + insertionTraversal: insertionTraversal, + }) + }); + + $(document).on('click', '.remove_fields, .remove_fields', function(e) { + e.preventDefault(); + + var $link = $(this); + var wrapperClass = $link.data('wrapper-class') || 'nested-fields'; + var nodeToDelete = $link.closest('.' + wrapperClass); + + var triggerNode = nodeToDelete.parent(); + + triggerNode.removeFormField(nodeToDelete) + }); + +})(jQuery); diff --git a/app/assets/javascripts/link_helpers.js b/app/assets/javascripts/link_helpers.js deleted file mode 100644 index 950eda4..0000000 --- a/app/assets/javascripts/link_helpers.js +++ /dev/null @@ -1,61 +0,0 @@ -(function($) { - - var createNewResourceID = function() { - return new Date().getTime(); - } - - $(document).on('click', '.add_fields', function(e) { - e.preventDefault(); - - var $link = $(this); - var assoc = $link.data('association'); - var content = $link.data('association-insertion-template'); - var insertionMethod = $link.data('association-insertion-method') || $link.data('association-insertion-position') || 'before'; - var insertionNode = $link.data('association-insertion-node'); - var insertionTraversal = $link.data('association-insertion-traversal'); - var newId = createNewResourceID(); - var regex = new RegExp("new_" + assoc, "g"); - var newContent = content.replace(regex, newId); - - if (insertionNode){ - if (insertionTraversal){ - insertionNode = $link[insertionTraversal](insertionNode); - } else { - insertionNode = insertionNode == "this" ? $link : $(insertionNode); - } - } else { - insertionNode = $link.parent(); - } - - var contentNode = $(newContent); - insertionNode.trigger('before-insert', [contentNode]); - - var addedContent = insertionNode[insertionMethod](contentNode); - - insertionNode.trigger('after-insert', [contentNode]); - }); - - $(document).on('click', '.remove_fields.dynamic, .remove_fields.existing', function(e) { - e.preventDefault(); - - var $link = $(this); - var wrapperClass = $link.data('wrapper-class') || 'nested-fields'; - var nodeToDelete = $link.closest('.' + wrapperClass); - var triggerNode = nodeToDelete.parent(); - - triggerNode.trigger('before-remove', [nodeToDelete]); - - var timeout = triggerNode.data('remove-timeout') || 0; - - setTimeout(function() { - if ($link.hasClass('dynamic')) { - nodeToDelete.remove(); - } else { - $link.prev("input[type=hidden]").val("1"); - nodeToDelete.hide(); - } - triggerNode.trigger('after-remove', [nodeToDelete]); - }, timeout); - }); - -})(jQuery); diff --git a/lib/active_form.rb b/lib/active_form.rb index 069fed5..8b8b985 100644 --- a/lib/active_form.rb +++ b/lib/active_form.rb @@ -10,7 +10,7 @@ class Engine < ::Rails::Engine config.before_initialize do if config.action_view.javascript_expansions - config.action_view.javascript_expansions[:link_helpers] = %w(link_helpers) + config.action_view.javascript_expansions[:activeform] = %w(activeform) end end diff --git a/lib/active_form/view_helpers.rb b/lib/active_form/view_helpers.rb index a207b1c..72b1b86 100644 --- a/lib/active_form/view_helpers.rb +++ b/lib/active_form/view_helpers.rb @@ -7,7 +7,7 @@ def link_to_remove_association(name, f, html_options={}) is_existing = f.object.persisted? classes << (is_existing ? 'existing' : 'dynamic') - + wrapper_class = html_options.delete(:wrapper_class) html_options[:class] = [html_options[:class], classes.join(' ')].compact.join(' ') html_options[:'data-wrapper-class'] = wrapper_class if wrapper_class.present? @@ -21,7 +21,7 @@ def link_to_remove_association(name, f, html_options={}) def render_association(association, f, new_object, render_options={}, custom_partial=nil) partial = get_partial_path(custom_partial, association) - + if f.respond_to?(:semantic_fields_for) method_name = :semantic_fields_for elsif f.respond_to?(:simple_fields_for) @@ -29,27 +29,30 @@ def render_association(association, f, new_object, render_options={}, custom_par else method_name = :fields_for end - + f.send(method_name, association, new_object, {:child_index => "new_#{association}"}.merge(render_options)) do |builder| render(partial: partial, locals: {:f => builder}) end end def link_to_add_association(name, f, association, html_options={}) - render_options = html_options.delete(:render_options) - render_options ||= {} - override_partial = html_options.delete(:partial) - html_options[:class] = [html_options[:class], "add_fields"].compact.join(' ') html_options[:'data-association'] = association.to_s + link_to(name, '#', html_options) + end + + def render_association_template(f, association, html_options={}) + render_options = html_options.delete(:render_options) + render_options ||= {} + override_partial = html_options.delete(:partial) new_object = create_object(f, association) - html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, render_options, override_partial).to_str).html_safe - - link_to(name, '#', html_options) + content_tag :template, class: "association_template #{association}_template" do + render_association(association, f, new_object, render_options, override_partial) + end end - + def create_object(f, association) f.object.get_model(association) end diff --git a/lib/rails/generators/form/form_install_generator.rb b/lib/rails/generators/form/form_install_generator.rb index 8b3b72e..b39c358 100644 --- a/lib/rails/generators/form/form_install_generator.rb +++ b/lib/rails/generators/form/form_install_generator.rb @@ -20,7 +20,7 @@ def create_forms_test_directory end def include_js_file - insert_into_file "app/assets/javascripts/application.js", "//= require link_helpers", :before => "//= require_tree ." + insert_into_file "app/assets/javascripts/application.js", "//= require activeform", :before => "//= require_tree ." end end end diff --git a/test/dummy/app/assets/javascripts/application.js b/test/dummy/app/assets/javascripts/application.js index 43ef054..d4149cf 100644 --- a/test/dummy/app/assets/javascripts/application.js +++ b/test/dummy/app/assets/javascripts/application.js @@ -12,5 +12,5 @@ // //= require jquery //= require jquery_ujs -//= require link_helpers +//= require activeform //= require_tree . diff --git a/test/dummy/app/assets/javascripts/projects.js b/test/dummy/app/assets/javascripts/projects.js index 687ad17..96d27e2 100644 --- a/test/dummy/app/assets/javascripts/projects.js +++ b/test/dummy/app/assets/javascripts/projects.js @@ -23,25 +23,20 @@ $(document).ready(function() { $(".project-tag-fields a.add_fields"). data("association-insertion-position", 'before'). data("association-insertion-node", 'this'); - $('.project-tag-fields').bind('after-insert', - function() { - $(this).children("#tag_from_list").remove(); - $(this).children("a.add_fields").hide(); - }); }); + $("body").on('after-insert', '.project-tag-fields', + function() { + $(this).children("#tag_from_list").remove(); + $(this).children("a.add_fields").hide(); + }); + $('#tasks').bind('before-insert', function(e,task_to_be_added) { task_to_be_added.fadeIn('slow'); }); - $('#tasks').bind('after-insert', function(e, added_task) { - //added_task.css("background","red"); - }); - $('#tasks').bind('before-remove', function(e, task) { $(this).data('remove-timeout', 1000); task.fadeOut('slow'); }) - - $('body').tabs(); -}); \ No newline at end of file +}); diff --git a/test/dummy/app/views/assignments/_form.html.erb b/test/dummy/app/views/assignments/_form.html.erb index b7c816f..e3f95ae 100644 --- a/test/dummy/app/views/assignments/_form.html.erb +++ b/test/dummy/app/views/assignments/_form.html.erb @@ -17,15 +17,22 @@

Tasks Details

- <%= f.fields_for :tasks do |task_fields| %> - <%= render "task_fields", :f => task_fields %> - <% end %> + + <%= render_association_template(f, :tasks) %> +
+ <%= f.fields_for :tasks do |task_fields| %> + <%= render "task_fields", :f => task_fields %> + <% end %> +
+ + +
<%= f.submit %>
-<% end %> \ No newline at end of file +<% end %> diff --git a/test/dummy/app/views/assignments/_task_fields.html.erb b/test/dummy/app/views/assignments/_task_fields.html.erb index e5022b3..80335e3 100644 --- a/test/dummy/app/views/assignments/_task_fields.html.erb +++ b/test/dummy/app/views/assignments/_task_fields.html.erb @@ -4,4 +4,4 @@ <%= f.text_field :name %> <%= link_to_remove_association "Delete", f %> - \ No newline at end of file + diff --git a/test/dummy/app/views/conferences/_speaker.html.erb b/test/dummy/app/views/conferences/_speaker.html.erb index 440d996..5969fbf 100644 --- a/test/dummy/app/views/conferences/_speaker.html.erb +++ b/test/dummy/app/views/conferences/_speaker.html.erb @@ -9,10 +9,13 @@

Presentantions

+<%= render_association_template f, :presentations %> +
<%= f.fields_for :presentations do |presentations_fields| %> - <%= render "presentation_fields", :f => presentations_fields %> + <%= render "presentation_fields", f: presentations_fields %> <% end %> +
\ No newline at end of file + <%= link_to_add_association "Add a Presentation", f, :presentations, data: { "association-insertion-node" => ".presentations_list" } %> + diff --git a/test/dummy/app/views/projects/_form.html.erb b/test/dummy/app/views/projects/_form.html.erb index 27f3649..34b4b35 100644 --- a/test/dummy/app/views/projects/_form.html.erb +++ b/test/dummy/app/views/projects/_form.html.erb @@ -1,5 +1,5 @@ <%= simple_form_for @project_form, wrapper: 'inline' do |f| %> - <%= f.input :name, :hint => 'The title of your project' %> + <%= f.input :name, hint: 'The title of your project' %>