1

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:

Deeply Nested Object Tree

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?

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.