13

I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.

Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.


Here's the json in the AngularJS front-end for the candidate:

{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}

Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:

{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}

The candidate_controller.rb contains a simple update:

class CandidatesController < ApplicationController

    before_filter :authenticate_user!

  respond_to :json

  def update
    respond_with Candidate.update(params[:id], candidate_params)
  end

private

  def candidate_params
    params.require(:candidate).permit(:nickname,
      works_attributes: [:id, :title, :description])
  end

end

The candidate.rb model includes the following code defining the has_many relationship with the works model:

class Candidate < ActiveRecord::Base

  ## Model Relationships
  belongs_to :users
  has_many :works, :dependent => :destroy  

  ## Nested model attributes
  accepts_nested_attributes_for :works, allow_destroy: true

  ## Validations
  validates_presence_of :nickname
  validates_uniqueness_of :user_id

end

And finally, the works.rb model defines the other side of the has_many relationship:

class Work < ActiveRecord::Base
  belongs_to :candidate
end

I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.

Thanks!

RTPnomad
  • 218
  • 2
  • 9
  • `Rails then translates this JSON into the following by adding the candidate header` what? Are you sure Rails does that? It's not the Job of Rails to change the payload of your client side App! – phoet Oct 25 '13 at 22:01
  • 2
    I'm fairly confident that Rails ActionController::ParamsWrapper takes the json input from Angularjs that does not have a root element and adds the piece that has the root element. I suppose my question would be: Is this causing the problem and why is the works_attributes array not included in this param wrapper change? – RTPnomad Oct 25 '13 at 23:48
  • please show me the code that you think is doing that. – phoet Oct 26 '13 at 12:47
  • phoet - Thanks for your line of questioning as it pointed me to one solution (posted below). Please let me know if there's a better way to manage the JSON interaction between AngularJS and Rails. – RTPnomad Oct 26 '13 at 16:08
  • Not sure if it is 'better' - I kind of like the idea of handling it on the server rather than the client - but you could add a request interceptor in Angular and wrap the request there. The documentation for interceptors is here: http://docs.angularjs.org/api/ng/service/$http – phillyslick Mar 24 '14 at 23:23

3 Answers3

7

I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:

class CandidatesController < ApplicationController
  respond_to :json

  nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key| 
    key.to_s.concat('_attributes').to_sym
  end

  wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
    format: :json

  # ...
end

Refer to this issue in Rails to see if/when they fix this problem.

Update 10/17
Pending a PR merge here: rails/rails#19254.

kmanzana
  • 1,138
  • 1
  • 13
  • 23
6

I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html

Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:

"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."

Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:

class CandidatesController < ApplicationController

    before_filter :authenticate_user!
    respond_to :json
    wrap_parameters include: [:id, :nickname, :works_attributes]
    ...

Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails

RTPnomad
  • 218
  • 2
  • 9
  • I'm not sure that I understand what you want to accomplish... I think the documentation you are referring to is only when you create JSON from a rails model using `render :json` – phoet Oct 26 '13 at 16:11
  • 3
    I'm using active model serializer to generate JSON output from rails. The paramswrapper facilitates incoming JSON per the documentation above: "Wraps the parameters hash into a nested hash. This will allow clients to submit POST requests without having to specify any root elements. – RTPnomad Oct 26 '13 at 16:40
  • I had the settle for the same solution – kmanzana Nov 18 '14 at 06:14
1

You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:

    module ActionController
        module ParamsWrapper

            Options.class_eval do
                def include
                    return super if @include_set

                    m = model
                    synchronize do
                        return super if @include_set
                        @include_set = true
                        unless super || exclude
                            if m.respond_to?(:attribute_names) && m.attribute_names.any?
                                self.include = m.attribute_names + nested_attributes_names_array_of(m)
                            end
                        end
                    end
                end

                private 
                    # added method. by default code was equivalent to this equaling to []
                    def nested_attributes_names_array_of model
                        model.nested_attributes_options.keys.map { |nested_attribute_name| 
                            nested_attribute_name.to_s + '_attributes' 
                        }
                    end
            end

        end
    end
Evgenia Karunus
  • 10,715
  • 5
  • 56
  • 70