Skip to content

Commit

Permalink
Support nested forms for has_many relationships
Browse files Browse the repository at this point in the history
Closes #192

Feature:

When I create or edit a model that has_many nested models,
I want to view and edit the attributes of the nested models
so I can set up all the relationships with a single form.

Implementation:

Introduce a new field type, `NestedHasMany`.

I considered building the feature into the existing `HasMany` field,
but this would get in the way of `HasMany` relationships
that aren't directly nested, such as in many-to-many relationships.

The `NestedHasMany` field builds off of the [Cocoon] gem.
It renders fields from the nested model,
based on the fields defined in the nested model's dashboard class.

Cocoon provides helpers and javascript
to easily add and remove nested form fields from the page.

[Cocoon]: https://github.com/nathanvda/cocoon
  • Loading branch information
c-lliope committed Apr 15, 2016
1 parent 7eb7ab9 commit 526d663
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 2 deletions.
1 change: 1 addition & 0 deletions administrate.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency "rails", "~> 4.2"
s.add_dependency "sass-rails", "~> 5.0"
s.add_dependency "selectize-rails", "~> 0.6"
s.add_dependency "cocoon", "~> 1.2"

s.description = <<-DESCRIPTION
Administrate is heavily inspired by projects like Rails Admin and ActiveAdmin,
Expand Down
7 changes: 5 additions & 2 deletions app/assets/javascripts/administrate/application.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//= require jquery
//= require jquery_ujs
//= require selectize
//= require moment

//= require cocoon
//= require datetime_picker
//= require moment
//= require selectize

//= require_tree .
12 changes: 12 additions & 0 deletions app/views/fields/nested_has_many/_fields.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="nested-fields" style="width:100%">

<%# TODO filter out the parent relationship, without this crude array slicing %>
<% field.associated_form.attributes[1..-1].each do |attribute| -%>
<div class="field">
<%= render_field attribute, f: f %>
</div>
<% end -%>

<%# TODO I18n %>
<%= link_to_remove_association "Remove #{field.associated_class_name.titleize}", f %>
</div>
24 changes: 24 additions & 0 deletions app/views/fields/nested_has_many/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="nested-fields" style="width: 40%; margin-left: 20%;">

<%= f.fields_for field.association_name do |nested_form| %>
<%= render(
partial: "fields/nested_has_many/fields",
locals: {
f: nested_form,
field: field,
},
) %>
<% end %>

<div class="links">
<%= link_to_add_association(
# TODO I18n
"Add #{field.associated_class_name.titleize}",
f,
field.association_name,
partial: "fields/nested_has_many/fields",
render_options: { locals: { field: field } },
) %>
</div>

</div>
19 changes: 19 additions & 0 deletions app/views/fields/nested_has_many/_index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<%#
# HasMany Index Partial
This partial renders a has_many relationship,
to be displayed on a resource's index page.
By default, the relationship is rendered
as a count of how many objects are associated through the relationship.
## Local variables:
- `field`:
An instance of [Administrate::Field::HasMany][1].
A wrapper around the has_many relationship pulled from the database.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/HasMany
%>

<%= pluralize(field.data.size, field.attribute.to_s.humanize.downcase) %>
40 changes: 40 additions & 0 deletions app/views/fields/nested_has_many/_show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<%#
# HasMany Show Partial
This partial renders a has_many relationship,
to be displayed on a resource's show page.
By default, the relationship is rendered
as a table of the first few associated resources.
The columns of the table are taken
from the associated resource class's dashboard.
## Local variables:
- `field`:
An instance of [Administrate::Field::HasMany][1].
Contains methods to help display a table of associated resources.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/HasMany
%>

<% if field.resources.any? %>
<%= render(
"collection",
collection_presenter: field.associated_collection,
resources: field.resources
) %>

<% if field.more_than_limit? %>
<span>
<%= t(
'administrate.fields.has_many.more',
count: field.limit,
total_count: field.data.count,
) %>
</span>
<% end %>

<% else %>
<%= t("administrate.fields.has_many.none") %>
<% end %>
1 change: 1 addition & 0 deletions lib/administrate/base_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require "administrate/field/has_many"
require "administrate/field/has_one"
require "administrate/field/image"
require "administrate/field/nested_has_many"
require "administrate/field/number"
require "administrate/field/polymorphic"
require "administrate/field/select"
Expand Down
1 change: 1 addition & 0 deletions lib/administrate/engine.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "cocoon"
require "datetime_picker_rails"
require "jquery-rails"
require "kaminari"
Expand Down
46 changes: 46 additions & 0 deletions lib/administrate/field/nested_has_many.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require_relative "has_many"
require "administrate/page/form"

module Administrate
module Field
class NestedHasMany < Administrate::Field::HasMany
DEFAULT_ATTRIBUTES = [:id, :_destroy].freeze

def to_s
data
end

protected

def self.dashboard_for_resource(resource)
"#{resource.to_s.classify}Dashboard".constantize
end

def self.associated_attributes(associated_resource)
DEFAULT_ATTRIBUTES +
dashboard_for_resource(associated_resource).new.permitted_attributes
end

public

def self.permitted_attribute(associated_resource)
{
"#{associated_resource}_attributes".to_sym =>
associated_attributes(associated_resource)
}
end

def associated_class_name
options.fetch(:class_name, attribute.to_s.singularize.camelcase)
end

def association_name
associated_class_name.underscore.pluralize
end

def associated_form
Administrate::Page::Form.new(associated_dashboard, association_name)
end
end
end
end

0 comments on commit 526d663

Please sign in to comment.