zijoud
zijoud

Reputation: 119

Updating deeply nested models with rails and angularjs

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?

Upvotes: 1

Views: 547

Answers (1)

zijoud
zijoud

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

Related Questions