Reputation: 119
I have researched this question very much, with no solution specifically for deeply nested models. I have come up with the following fix, wondering if there is a better way.
I've tried to cut this down to be nearly as simple as I can make it to explain the issue and my solution.
The issue is that there exist the following ActiveRecord models in rails:
class Template
has_many :sections, dependent: :destroy
accepts_nested_attributes_for :sections
has_many :columns, through: :sections, dependent: :destroy
has_many :fields, through: :columns, dependent: :destroy
class Section
belongs_to :template
has_many :columns, dependent: :destroy
accepts_nested_attributes_for :columns
class Column
belongs_to :section
has_many :fields, dependent: :destroy
accepts_nested_attributes_for :fields
class Field
belongs_to :column
and in angular, the goal is to send a single ngResource $resource call to 'templates/:id' and update the entire chain of children. (Each piece of the chain is created prior, in the template creation process. The need for a unified update occurs when the template is finalized.)
### ClassFactory.coffee ###
#
# Create a connection to the API
# ...
return $resource('api/:class/:id', { format: 'json' }, {...})
# ...
### EditTemplateController.coffee ###
#
# Create angular template resource, save it, and redirect to editing view
# ...
$scope.template = new ClassFactory({class: 'templates'})
$scope.template.$save({class: 'templates'}, (res)->
$location.path('templates/'+res.id+'/edit')
)
#
# Update the angular object
# ...
$scope.saveTemplate = ->
$scope.template.$update({class: 'templates', id: $scope.template.id})
#...
### templates_controller.rb ###
#
# Create a single DB interaction with deliberately declared parameters and nested *_attributes parameters
# ...
def update
params[:template][:sections_attributes] = params[:sections]
params[:template][:sections_attributes].each do |paramSection|
paramSection[:columns_attributes] = paramSection[:columns]
paramSection[:columns_attributes].each do |paramColumn|
paramColumn[:fields_attributes] = paramColumn[:fields]
end
end
template = current_user.templates.find(params[:id])
template.update_attributes(allowed_params)
head :no_content
end
private
def allowed_params
params.require(:template).permit(
:name, sections_attributes: [
:id, :name, columns_attributes: [
:id, :fields_attributes: [
:id, :name, :value
]
]
]
end
# ...
As far as I have worked out, the fix is to declare *_attributes as shown above:
params[:template][:sections_attributes] = params[:sections]
because of angular's inability to send the format of parameters that rails is looking for.
This obviously feels like a hacky solution. Is there no better way to handle deeply nested rails models while using angularjs?
Upvotes: 1
Views: 547
Reputation: 119
As discussed in this Rails github issue, this is an acknowledged issue with the way that AngularJS $resource sends parameters, versus what Rails expects when using accepts_nested_attributes_for
.
Per that issue, and until this is resolved in a Rails fix, here is what can be changed in the above example to separate its parts to be a bit more manageable:
Add to any rails controller which model uses accepts_nested_attributes_for
:
class ModelController < ApplicationController
nested_attributes_names = Model.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Model.attribute_names + nested_attributes_names
# ...
end
Clean up the Rails controller Update method by moving the nested *_attributes
declarations to the AngularJS controller before saving the model:
$scope.saveTemplate = ->
$scope.template.sections_attributes = $scope.template.sections
$scope.template.sections_attributes.forEach((section)->
section.columns_attributes = section.columns
section.columns_attributes.forEach((column)->
column.fields_attributes = column.fields
)
)
$scope.template.$update({class: 'templates', id: $scope.template.id})
It's not pretty but that's seemingly all that can be done for this very specific issue until it's patched.
Upvotes: 1