require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' module ActiveModel # == Active Model Dirty # # Provides a way to track changes in your object in the same way as # Active Record does. # # The requirements for implementing ActiveModel::Dirty are: # # * include ActiveModel::Dirty in your object # * Call define_attribute_methods passing each method you want to # track # * Call attr_name_will_change! before each change to the tracked # attribute # # If you wish to also track previous changes on save or update, you need to # add # # @previously_changed = changes # # inside of your save or update method. # # A minimal implementation could be: # # class Person # # include ActiveModel::Dirty # # define_attribute_methods = [:name] # # def name # @name # end # # def name=(val) # name_will_change! unless val == @name # @name = val # end # # def save # @previously_changed = changes # @changed_attributes.clear # end # # end # # == Examples: # # A newly instantiated object is unchanged: # person = Person.find_by_name('Uncle Bob') # person.changed? # => false # # Change the name: # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true # person.name_was # => 'Uncle Bob' # person.name_change # => ['Uncle Bob', 'Bob'] # person.name = 'Bill' # person.name_change # => ['Uncle Bob', 'Bill'] # # Save the changes: # person.save # person.changed? # => false # person.name_changed? # => false # # Assigning the same value leaves the attribute unchanged: # person.name = 'Bill' # person.name_changed? # => false # person.name_change # => nil # # Which attributes have changed? # person.name = 'Bob' # person.changed # => ['name'] # person.changes # => { 'name' => ['Bill', 'Bob'] } # # If an attribute is modified in-place then make use of [attribute_name]_will_change! # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to # in-place attributes. # # person.name_will_change! # person.name << 'y' # person.name_change # => ['Bill', 'Billy'] module Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' attribute_method_affix :prefix => 'reset_', :suffix => '!' end # Returns true if any attribute have unsaved changes, false otherwise. # person.changed? # => false # person.name = 'bob' # person.changed? # => true def changed? !changed_attributes.empty? end # List of attributes with unsaved changes. # person.changed # => [] # person.name = 'bob' # person.changed # => ['name'] def changed changed_attributes.keys end # Map of changed attrs => [original value, new value]. # person.changes # => {} # person.name = 'bob' # person.changes # => { 'name' => ['bill', 'bob'] } def changes HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end # Map of attributes that were changed when the model was saved. # person.name # => 'bob' # person.name = 'robert' # person.save # person.previous_changes # => {'name' => ['bob, 'robert']} def previous_changes @previously_changed end # Map of change attr => original value. def changed_attributes @changed_attributes ||= {} end private # Handle *_changed? for +method_missing+. def attribute_changed?(attr) changed_attributes.include?(attr) end # Handle *_change for +method_missing+. def attribute_change(attr) [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) end # Handle *_was for +method_missing+. def attribute_was(attr) attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end # Handle *_will_change! for +method_missing+. def attribute_will_change!(attr) begin value = __send__(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end changed_attributes[attr] = value unless changed_attributes.include?(attr) end # Handle reset_*! for +method_missing+. def reset_attribute!(attr) __send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr) end end end