Module: ActiveModel::Datastore::NestedAttr

Extended by:
ActiveSupport::Concern
Includes:
Model
Included in:
ActiveModel::Datastore
Defined in:
lib/active_model/datastore/nested_attr.rb

Overview

ActiveModel Datastore Nested Attributes

Adds support for nested attributes to ActiveModel. Heavily inspired by Rails ActiveRecord::NestedAttributes.

Nested attributes allow you to save attributes on associated records along with the parent. It’s used in conjunction with fields_for to build the nested form elements.

See Rails ActionView::Helpers::FormHelper::fields_for for more info.

NOTE: Unlike ActiveRecord, the way that the relationship is modeled between the parent and child is not enforced. With NoSQL the relationship could be defined by any attribute, or with denormalization exist within the same entity. This library provides a way for the objects to be associated yet saved to the datastore in any way that you choose.

You enable nested attributes by defining an :attr_accessor on the parent with the pluralized name of the child model.

Nesting also requires that a <association_name>_attributes= writer method is defined in your parent model. If an object with an association is instantiated with a params hash, and that hash has a key for the association, Rails will call the <association_name>_attributes= method on that object. Within the writer method call assign_nested_attributes, passing in the association name and attributes.

Let’s say we have a parent Recipe with Ingredient children.

Start by defining within the Recipe model:

  • an attr_accessor of :ingredients

  • a writer method named ingredients_attributes=

  • the validates_associated method can be used to validate the nested objects

Example:

class Recipe
  attr_accessor :ingredients
  validates :ingredients, presence: true
  validates_associated :ingredients

  def ingredients_attributes=(attributes)
    assign_nested_attributes(:ingredients, attributes)
  end
end

You may also set a :reject_if proc to silently ignore any new record hashes if they fail to pass your criteria. For example:

class Recipe
  def ingredients_attributes=(attributes)
    reject_proc = proc { |attributes| attributes['name'].blank? }
    assign_nested_attributes(:ingredients, attributes, reject_if: reject_proc)
  end
end

Alternatively, :reject_if also accepts a symbol for using methods:

class Recipe
  def ingredients_attributes=(attributes)
    reject_proc = proc { |attributes| attributes['name'].blank? }
    assign_nested_attributes(:ingredients, attributes, reject_if: reject_recipes)
  end

  def reject_recipes(attributes)
    attributes['name'].blank?
  end
end

Within the parent model valid? will validate the parent and associated children and nested_models will return the child objects. If the nested form submitted params contained a truthy _destroy key, the appropriate nested_models will have marked_for_destruction set to True.

Created by Bryce McLean on 2016-12-06.

Defined Under Namespace

Modules: ClassMethods Classes: AssociatedValidator

Instance Method Summary collapse

Instance Method Details

#assign_nested_attributes(association_name, attributes, options = {}) ⇒ Object

Assigns the given nested child attributes.

Attribute hashes with an :id value matching an existing associated object will update that object. Hashes without an :id value will build a new object for the association. Hashes with a matching :id value and a :_destroy key set to a truthy value will mark the matched object for destruction.

Pushes a key of the association name onto the parent object’s nested_attributes attribute. The nested_attributes can be used for determining when the parent has associated children.

The following example will update the amount of the ingredient with ID 1, build a new associated ingredient with the amount of 45, and mark the associated ingredient with ID 2 for destruction.

assign_nested_attributes(:ingredients, {
  '0' => { id: '1', amount: '123' },
  '1' => { amount: '45' },
  '2' => { id: '2', _destroy: true }
})

Parameters:

  • association_name (Symbol)

    The attribute name of the associated children.

  • attributes (ActiveSupport::HashWithIndifferentAccess, ActionController::Parameters)

    The attributes provided by Rails ActionView. Typically new objects will arrive as ActiveSupport::HashWithIndifferentAccess and updates as ActionController::Parameters.

  • options (Hash) (defaults to: {})

    The options to control how nested attributes are applied.

Options Hash (options):

  • :reject_if (Proc, Symbol)

    Allows you to specify a Proc or a Symbol pointing to a method that checks whether a record should be built for a certain attribute hash. The hash is passed to the supplied Proc or the method and it should return either true or false. Passing :all_blank instead of a Proc will create a proc that will reject a record where all the attributes are blank.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/active_model/datastore/nested_attr.rb', line 155

def assign_nested_attributes(association_name, attributes, options = {})
  attributes = validate_attributes(attributes)
  association_name = association_name.to_sym
  send("#{association_name}=", []) if send(association_name).nil?

  attributes.each_value do |params|
    if params['id'].blank?
      unless reject_new_record?(params, options)
        new = association_name.to_c.new(params.except(*UNASSIGNABLE_KEYS))
        send(association_name).push(new)
      end
    else
      existing = send(association_name).detect { |record| record.id.to_s == params['id'].to_s }
      assign_to_or_mark_for_destruction(existing, params)
    end
  end
  (self.nested_attributes ||= []).push(association_name)
end

#mark_for_destructionObject



85
86
87
# File 'lib/active_model/datastore/nested_attr.rb', line 85

def mark_for_destruction
  @marked_for_destruction = true
end

#marked_for_destruction?Boolean

Returns:

  • (Boolean)


89
90
91
# File 'lib/active_model/datastore/nested_attr.rb', line 89

def marked_for_destruction?
  @marked_for_destruction
end

#nested_attributes?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/active_model/datastore/nested_attr.rb', line 93

def nested_attributes?
  nested_attributes.is_a?(Array) && !nested_attributes.empty?
end

#nested_errorsObject



112
113
114
115
116
117
118
119
120
# File 'lib/active_model/datastore/nested_attr.rb', line 112

def nested_errors
  errors = []
  if nested_attributes?
    nested_attributes.each do |attr|
      send(attr.to_sym).each { |child| errors << child.errors }
    end
  end
  errors
end

#nested_model_class_namesObject



106
107
108
109
110
# File 'lib/active_model/datastore/nested_attr.rb', line 106

def nested_model_class_names
  entity_kinds = []
  nested_models.each { |x| entity_kinds << x.class.name } if nested_attributes?
  entity_kinds.uniq
end

#nested_modelsObject

For each attribute name in nested_attributes extract and return the nested model objects.



100
101
102
103
104
# File 'lib/active_model/datastore/nested_attr.rb', line 100

def nested_models
  model_entities = []
  nested_attributes.each { |attr| model_entities << send(attr.to_sym) } if nested_attributes?
  model_entities.flatten
end