DEV Community

Mateusz Utkała
Mateusz Utkała

Posted on

Use removable models in Rails nested attributes

In many Rails based systems we can find models that should not be destroyed but should be tagged as removed/disabled.
This is a simple problem to solve but it becomes complicated when we want to use accepts_nested_attributes_for ... , allow_destroy: true

I want to share with you my latest thoughts about this problem. Maybe you have some fancy solution.

Usually I use simple ActiveSupport::Concern to satisfy the bussines

module Removable
  extend ActiveSupport::Concern

  included do
    scope :removed, -> { where(removed: true) }
    scope :active, -> { where(removed: false) }
  end

  def removed?
    removed
  end

  def remove(user)
    update!({
              removed: true,
              removed_by: user,
              removed_date: DateTime.now
            })
    # other bussines logic for remove
  end

  def remove_nested(user)
    self.removed = true
    self.removed_by = user
    self.removed_date: DateTime.now
    # other bussines logic for remove
  end

  def restore
    # bussines logic for restore
  end
Enter fullscreen mode Exit fullscreen mode

No matter how you implemented nested attributes (Stimulus, Old Rails Way, cocoon, etc..), the rails magic is the same. As always we need to permit controller parameters for our nested attributes: id and _destroy:

def todo_params
  params.require(:todo).permit(:name, subtasks_attributes: %i[ title content id _destroy])
end
Enter fullscreen mode Exit fullscreen mode

At this point rails magic should destroy nested models.

Important notice

It destroy record due to Active Record Autosave Association #marked_for_destruction

Marks this record to be destroyed as part of the parent's save transaction. This does not actually destroy the record instantly, rather child record will be destroyed when parent.save is called.

Using default controller action like @todo.update(todo_params) we don't have any option to access and change destroy behaivior to use Removable#remove_nested.

My current solution

Lets provide before_validation :mark_nested_removed to take care about almost destroyed records.

  def mark_nested_removed
    subtasks.filter(&:marked_for_destruction?).each do |subtask|
      subtask.reload
      subtask.remove_nested(edited_by)
    end
  end
Enter fullscreen mode Exit fullscreen mode
  • we use subtask.reload to clear marked_for_destruction flag, that prevent destroying record.
  • we also use subtask.remove_nested to set params to model instead of make update! immediately

What do YOU think about it?

__
If you found these helpful, share them with your mates.

References:

Top comments (1)

Collapse
 
bcasci profile image
Brandon Casci

Nice!