123456789_123456789_123456789_123456789_123456789_

Module: ActiveModel::Dirty

Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Included In:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Defined in: activemodel/lib/active_model/dirty.rb

Overview

Provides a way to track changes in your object in the same way as Active Record does.

The requirements for implementing Dirty are:

  • include ActiveModel::Dirty in your object.

  • Call define_attribute_methods passing each method you want to track.

  • Call *_will_change! before each change to the tracked attribute.

  • Call #changes_applied after the changes are persisted.

  • Call #clear_changes_information when you want to reset the changes information.

  • Call #restore_attributes when you want to restore previous data.

A minimal implementation could be:

class Person
  include ActiveModel::Dirty

  define_attribute_methods :name

  def initialize
    @name = nil
  end

  def name
    @name
  end

  def name=(val)
    name_will_change! unless val == @name
    @name = val
  end

  def save
    # do persistence work

    changes_applied
  end

  def reload!
    # get the values from the persistence layer

    clear_changes_information
  end

  def rollback!
    restore_attributes
  end
end

A newly instantiated Person object is unchanged:

person = Person.new
person.changed? # => false

Change the name:

person.name = 'Bob'
person.changed?       # => true
person.name_changed?  # => true
person.name_changed?(from: nil, to: "Bob") # => true
person.name_was       # => nil
person.name_change    # => [nil, "Bob"]
person.name = 'Bill'
person.name_change    # => [nil, "Bill"]

Save the changes:

person.save
person.changed?      # => false
person.name_changed? # => false

Reset the changes:

person.previous_changes         # => {"name" => [nil, "Bill"]}
person.name_previously_changed? # => true
person.name_previously_changed?(from: nil, to: "Bill") # => true
person.name_previous_change     # => [nil, "Bill"]
person.name_previously_was      # => nil
person.reload!
person.previous_changes         # => {}

Rollback the changes:

person.name = "Uncle Bob"
person.rollback!
person.name          # => "Bill"
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 *_will_change! to mark that the attribute is changing. Otherwise Active Model can’t track changes to in-place attributes. Note that Active Record can detect in-place modifications automatically. You do not need to call *_will_change! on Active Record models.

person.name_will_change!
person.name_change # => ["Bill", "Bill"]
person.name << 'y'
person.name_change # => ["Bill", "Billy"]

Methods can be invoked as name_changed? or by passing an argument to the generic method attribute_changed?("name").

Constant Summary

AttributeMethods - Included

CALL_COMPILABLE_REGEXP, NAME_COMPILABLE_REGEXP

AttributeMethods - Attributes & Methods

Class Method Summary

::ActiveSupport::Concern - Extended

class_methods

Define class methods from given block.

included

Evaluate given block in context of base class, so that you can write class macros here.

prepended

Evaluate given block in context of base class, so that you can write class macros here.

append_features, prepend_features

Instance Attribute Summary

Instance Method Summary

AttributeMethods - Included

#attribute_missing

attribute_missing is like method_missing, but for attributes.

#method_missing

Allows access to the object attributes, which are held in the hash returned by attributes, as though they were first-class methods.

#respond_to?,
#respond_to_without_attributes?

A Person instance with a name attribute can ask person.respond_to?(:name), person.respond_to?(:name=), and person.respond_to?(:name?) which will all return true.

#_read_attribute, #attribute_method?,
#matched_attribute_method

Returns a struct representing the matching attribute method.

#missing_attribute

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class ActiveModel::AttributeMethods

DSL Calls

included

[ GitHub ]


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'activemodel/lib/active_model/dirty.rb', line 127

included do
  ##
  # :method: *_previously_changed?
  #
  # :call-seq: *_previously_changed?(**options)
  #
  # This method is generated for each attribute.
  #
  # Returns true if the attribute previously had unsaved changes.
  #
  #   person = Person.new
  #   person.name = 'Britanny'
  #   person.save
  #   person.name_previously_changed? # => true
  #   person.name_previously_changed?(from: nil, to: 'Britanny') # => true

  ##
  # :method: *_changed?
  #
  # This method is generated for each attribute.
  #
  # Returns true if the attribute has unsaved changes.
  #
  #   person = Person.new
  #   person.name = 'Andrew'
  #   person.name_changed? # => true

  ##
  # :method: *_change
  #
  # This method is generated for each attribute.
  #
  # Returns the old and the new value of the attribute.
  #
  #   person = Person.new
  #   person.name = 'Nick'
  #   person.name_change # => [nil, 'Nick']

  ##
  # :method: *_will_change!
  #
  # This method is generated for each attribute.
  #
  # If an attribute is modified in-place then make use of
  # <tt>*_will_change!</tt> to mark that the attribute is changing.
  # Otherwise Active Model can’t track changes to in-place attributes. Note
  # that Active Record can detect in-place modifications automatically. You
  # do not need to call <tt>*_will_change!</tt> on Active Record
  # models.
  #
  #   person = Person.new('Sandy')
  #   person.name_will_change!
  #   person.name_change # => ['Sandy', 'Sandy']

  ##
  # :method: *_was
  #
  # This method is generated for each attribute.
  #
  # Returns the old value of the attribute.
  #
  #   person = Person.new(name: 'Steph')
  #   person.name = 'Stephanie'
  #   person.name_was # => 'Steph'

  ##
  # :method: *_previous_change
  #
  # This method is generated for each attribute.
  #
  # Returns the old and the new value of the attribute before the last save.
  #
  #   person = Person.new
  #   person.name = 'Emmanuel'
  #   person.save
  #   person.name_previous_change # => [nil, 'Emmanuel']

  ##
  # :method: *_previously_was
  #
  # This method is generated for each attribute.
  #
  # Returns the old value of the attribute before the last save.
  #
  #   person = Person.new
  #   person.name = 'Sage'
  #   person.save
  #   person.name_previously_was  # => nil

  ##
  # :method: restore_*!
  #
  # This method is generated for each attribute.
  #
  # Restores the attribute to the old value.
  #
  #   person = Person.new
  #   person.name = 'Amanda'
  #   person.restore_name!
  #   person.name # => nil

  ##
  # :method: clear_*_change
  #
  # This method is generated for each attribute.
  #
  # Clears all dirty data of the attribute: current changes and previous changes.
  #
  #   person = Person.new(name: 'Chris')
  #   person.name = 'Jason'
  #   person.name_change # => ['Chris', 'Jason']
  #   person.clear_name_change
  #   person.name_change # => nil

  attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
  attribute_method_suffix "_change", "_will_change!", "_was", parameters: false
  attribute_method_suffix "_previous_change", "_previously_was", parameters: false
  attribute_method_affix prefix: "restore_", suffix: "!", parameters: false
  attribute_method_affix prefix: "clear_", suffix: "_change", parameters: false
end

Class Attribute Details

.attribute_aliases (rw)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 71

class_attribute :attribute_aliases, instance_writer: false, default: {}

.attribute_aliases?Boolean (rw)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 71

class_attribute :attribute_aliases, instance_writer: false, default: {}

.attribute_method_patterns (rw)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 72

class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]

.attribute_method_patterns?Boolean (rw)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 72

class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]

Instance Attribute Details

#attribute_aliases (readonly)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 71

class_attribute :attribute_aliases, instance_writer: false, default: {}

#attribute_aliases?Boolean (readonly)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 71

class_attribute :attribute_aliases, instance_writer: false, default: {}

#attribute_method_patterns (readonly)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 72

class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]

#attribute_method_patterns?Boolean (readonly)

[ GitHub ]

  
# File 'activemodel/lib/active_model/attribute_methods.rb', line 72

class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]

#changed?Boolean (readonly)

Returns true if any of the attributes has unsaved changes, false otherwise.

person.changed? # => false
person.name = 'bob'
person.changed? # => true
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 279

def changed?
  mutations_from_database.any_changes?
end

Instance Method Details

#as_json(options = {})

This method is for internal use only.
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 258

def as_json(options = {}) # :nodoc:
  options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
  super(options)
end

#attribute_change(attr_name) (private)

Dispatch target for *_change attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 392

def attribute_change(attr_name)
  mutations_from_database.change_to_attribute(attr_name.to_s)
end

#attribute_changed?(attr_name, **options) ⇒ Boolean

Dispatch target for *_changed? attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 293

def attribute_changed?(attr_name, **options)
  mutations_from_database.changed?(attr_name.to_s, **options)
end

#attribute_changed_in_place?(attr_name) ⇒ Boolean

This method is for internal use only.
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 360

def attribute_changed_in_place?(attr_name) # :nodoc:
  mutations_from_database.changed_in_place?(attr_name.to_s)
end

#attribute_previous_change(attr_name) (private)

Dispatch target for *_previous_change attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 397

def attribute_previous_change(attr_name)
  mutations_before_last_save.change_to_attribute(attr_name.to_s)
end

#attribute_previously_changed?(attr_name, **options) ⇒ Boolean

Dispatch target for *_previously_changed? attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 303

def attribute_previously_changed?(attr_name, **options)
  mutations_before_last_save.changed?(attr_name.to_s, **options)
end

#attribute_previously_was(attr_name)

Dispatch target for *_previously_was attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 308

def attribute_previously_was(attr_name)
  mutations_before_last_save.original_value(attr_name.to_s)
end

#attribute_was(attr_name)

Dispatch target for *_was attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 298

def attribute_was(attr_name)
  mutations_from_database.original_value(attr_name.to_s)
end

#attribute_will_change!(attr_name) (private)

Dispatch target for *_will_change! attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 402

def attribute_will_change!(attr_name)
  mutations_from_database.force_change(attr_name.to_s)
end

#changed (readonly)

Returns an array with the name of the attributes with unsaved changes.

person.changed # => []
person.name = 'bob'
person.changed # => ["name"]
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 288

def changed
  mutations_from_database.changed_attribute_names
end

#changed_attributes

Returns a hash of the attributes with unsaved changes indicating their original values like attr => original value.

person.name # => "bob"
person.name = 'robert'
person.changed_attributes # => {"name" => "bob"}
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 336

def changed_attributes
  mutations_from_database.changed_values
end

#changes

Returns a hash of changed attributes indicating their original and new values like attr => [original value, new value].

person.changes # => {}
person.name = 'bob'
person.changes # => { "name" => ["bill", "bob"] }
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 346

def changes
  mutations_from_database.changes
end

#changes_applied

Clears dirty data and moves #changes to #previous_changes and #mutations_from_database to #mutations_before_last_save respectively.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 265

def changes_applied
  unless defined?(@attributes)
    mutations_from_database.finalize_changes
  end
  @mutations_before_last_save = mutations_from_database
  forget_attribute_assignments
  @mutations_from_database = nil
end

#clear_attribute_change(attr_name) (private)

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 371

def clear_attribute_change(attr_name)
  mutations_from_database.forget_change(attr_name.to_s)
end

#clear_attribute_changes(attr_names)

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 324

def clear_attribute_changes(attr_names)
  attr_names.each do |attr_name|
    clear_attribute_change(attr_name)
  end
end

#clear_changes_information

Clears all dirty data: current changes and previous changes.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 318

def clear_changes_information
  @mutations_before_last_save = nil
  forget_attribute_assignments
  @mutations_from_database = nil
end

#forget_attribute_assignments (private)

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 383

def forget_attribute_assignments
  @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
end

#init_internals (private)

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 365

def init_internals
  super
  @mutations_before_last_save = nil
  @mutations_from_database = nil
end

#initialize_dup(other)

This method is for internal use only.
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 248

def initialize_dup(other) # :nodoc:
  super
  if self.class.respond_to?(:_default_attributes)
    @attributes = self.class._default_attributes.map do |attr|
      attr.with_value_from_user(@attributes.fetch_value(attr.name))
    end
  end
  @mutations_from_database = nil
end

#mutations_before_last_save (private)

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 387

def mutations_before_last_save
  @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
end

#mutations_from_database (private)

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 375

def mutations_from_database
  @mutations_from_database ||= if defined?(@attributes)
    ActiveModel::AttributeMutationTracker.new(@attributes)
  else
    ActiveModel::ForcedMutationTracker.new(self)
  end
end

#previous_changes

Returns a hash of attributes that were changed before the model was saved.

person.name # => "bob"
person.name = 'robert'
person.save
person.previous_changes # => {"name" => ["bob", "robert"]}
[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 356

def previous_changes
  mutations_before_last_save.changes
end

#restore_attribute!(attr_name) (private)

Dispatch target for restore_*! attribute methods.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 407

def restore_attribute!(attr_name)
  attr_name = attr_name.to_s
  if attribute_changed?(attr_name)
    __send__("#{attr_name}=", attribute_was(attr_name))
    clear_attribute_change(attr_name)
  end
end

#restore_attributes(attr_names = changed)

Restore all previous data of the provided attributes.

[ GitHub ]

  
# File 'activemodel/lib/active_model/dirty.rb', line 313

def restore_attributes(attr_names = changed)
  attr_names.each { |attr_name| restore_attribute!(attr_name) }
end