Class: Glimmer::DataBinding::ModelBinding

Inherits:
Object
  • Object
show all
Includes:
Observable, Observer
Defined in:
lib/glimmer/data_binding/model_binding.rb

Constant Summary collapse

ARRAY_INDEXED_PROPERTY_ARGUMENT_REGEX =
/\d+/
HASH_SYMBOL_INDEXED_PROPERTY_ARGUMENT_REGEX =
/:[^:]+/
HASH_SINGLE_QUOTE_INDEXED_PROPERTY_ARGUMENT_REGEX =
/'[^']+'/
HASH_DOUBLE_QUOTE_INDEXED_PROPERTY_ARGUMENT_REGEX =
/"[^"]+"/

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Observer

#add_dependent, #compact_args, #dependents, #dependents_for, #observe, proc, #registration_for, #registrations, #remove_dependent, #unobserve, #unobserve_all_observables, #unobserve_dependents_with_observable

Methods included from Observable

#inspect

Constructor Details

#initialize(*args) ⇒ ModelBinding

Returns a new instance of ModelBinding.



38
39
40
41
42
43
44
45
46
47
# File 'lib/glimmer/data_binding/model_binding.rb', line 38

def initialize(*args)
  binding_options = args.pop if args.size > 1 && args.last.is_a?(Hash)
  @base_model, @property_name_expression = args
  @binding_options = binding_options || Concurrent::Hash.new
  if computed?
    @computed_model_bindings = Concurrent::Array.new(computed_by.map do |computed_by_property_expression|
      self.class.new(base_model, computed_by_property_expression)
    end)
  end
end

Instance Attribute Details

#binding_optionsObject (readonly)

Returns the value of attribute binding_options.



36
37
38
# File 'lib/glimmer/data_binding/model_binding.rb', line 36

def binding_options
  @binding_options
end

#property_name_expressionObject (readonly)

Returns the value of attribute property_name_expression.



36
37
38
# File 'lib/glimmer/data_binding/model_binding.rb', line 36

def property_name_expression
  @property_name_expression
end

Instance Method Details

#add_computed_observers(observer) ⇒ Object



180
181
182
183
184
185
186
# File 'lib/glimmer/data_binding/model_binding.rb', line 180

def add_computed_observers(observer)
  @computed_model_bindings.each do |computed_model_binding|
    observer_registration = computed_observer_for(observer).observe(computed_model_binding, observation_options)
    my_registration = observer.registration_for(self)
    observer.add_dependent(my_registration => observer_registration)
  end
end

#add_nested_observers(observer) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/glimmer/data_binding/model_binding.rb', line 188

def add_nested_observers(observer)
  nested_property_observers = nested_property_observers_for(observer)
  Concurrent::Array.new(nested_models.zip(nested_property_names)).each_with_index do |zip, i|
    model, property_name = zip
    nested_property_observer = nested_property_observers[property_name]
    previous_index = i - 1
    if previous_index.negative?
      parent_model = self
      parent_property_name = nil
      parent_observer = observer
    else
      parent_model = nested_models[previous_index]
      parent_property_name = nested_property_names[previous_index]
      parent_observer = nested_property_observers[parent_property_name]
    end
    parent_property_name = nil if parent_property_name.to_s.start_with?('[')
    unless model.nil?
      # TODO figure out a way to deal with this more uniformly
      observer_registration = property_indexed?(property_name) ? nested_property_observer.observe(model, observation_options) : nested_property_observer.observe(model, property_name, observation_options)
      parent_registration = parent_observer.registration_for(parent_model, *[parent_property_name].compact)
      parent_observer.add_dependent(parent_registration => observer_registration)
    end
  end
end

#add_observer(observer, extra_options = {}) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/glimmer/data_binding/model_binding.rb', line 133

def add_observer(observer, extra_options = {})
  # TODO couldn't we have a scenario where it is both computed? and nested_property? at the same time?
  # or computed and not nested at the same time (else statement)?
  if computed?
    add_computed_observers(observer)
  elsif nested_property?
    add_nested_observers(observer)
  else
    model_binding_observer = Observer.proc do |new_value|
      converted_value = evaluate_property
      observer.call(converted_value).tap do
        apply_processor(@binding_options[:after_read], converted_value)
      end
    end
    observer_registration = model_binding_observer.observe(*([model] + [property_name, observation_options].compact))
    my_registration = observer.registration_for(self)
    observer.add_dependent(my_registration => observer_registration)
  end
end

#base_modelObject



70
71
72
# File 'lib/glimmer/data_binding/model_binding.rb', line 70

def base_model
  @base_model
end

#call(value, *extra_args) ⇒ Object



213
214
215
216
217
# File 'lib/glimmer/data_binding/model_binding.rb', line 213

def call(value, *extra_args)
  return if model.nil?
  converted_value = value
  invoke_property_writer(model, model.is_a?(Hash) && !property_indexed?(property_name) ? property_name : "#{property_name}=", converted_value) unless converted_value == evaluate_property || property_name.nil?
end

#computed?Boolean

Returns:

  • (Boolean)


102
103
104
# File 'lib/glimmer/data_binding/model_binding.rb', line 102

def computed?
  !computed_by.empty?
end

#computed_byObject



106
107
108
# File 'lib/glimmer/data_binding/model_binding.rb', line 106

def computed_by
  Concurrent::Array.new([@binding_options[:computed_by]].flatten.compact)
end

#computed_observer_for(observer) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/glimmer/data_binding/model_binding.rb', line 167

def computed_observer_for(observer)
  @computed_observer_collection ||= Concurrent::Hash.new
  unless @computed_observer_collection.has_key?(observer)
    @computed_observer_collection[observer] = Observer.proc do |new_value|
      converted_value = evaluate_property
      observer.call(converted_value).tap do
        apply_processor(@binding_options[:after_read], converted_value)
      end
    end
  end
  @computed_observer_collection[observer]
end

#evaluate_options_propertyObject



226
227
228
# File 'lib/glimmer/data_binding/model_binding.rb', line 226

def evaluate_options_property
  model.send(options_property_name) unless model.nil?
end

#evaluate_propertyObject



219
220
221
222
223
224
# File 'lib/glimmer/data_binding/model_binding.rb', line 219

def evaluate_property
  value = nil
  value = invoke_property_reader(model, property_name) unless model.nil?
  apply_processor(@binding_options[:before_read], value)
  convert_on_read(value)
end

#modelObject



49
50
51
# File 'lib/glimmer/data_binding/model_binding.rb', line 49

def model
  nested_property? ? nested_model : base_model
end

#model_property_namesObject

Model representing nested property names e.g. property name expression “address.state” gives [‘address’]



94
95
96
# File 'lib/glimmer/data_binding/model_binding.rb', line 94

def model_property_names
  Concurrent::Array.new(nested_property_names[0...-1])
end

#nested_modelObject



66
67
68
# File 'lib/glimmer/data_binding/model_binding.rb', line 66

def nested_model
  nested_models.last
end

#nested_modelsObject

e.g. person.address.state returns [person, person.address]



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/glimmer/data_binding/model_binding.rb', line 54

def nested_models
  @nested_models = Concurrent::Array.new([base_model])
  model_property_names.reduce(base_model) do |reduced_model, nested_model_property_name|
    if !reduced_model.nil?
      invoke_property_reader(reduced_model, nested_model_property_name).tap do |new_reduced_model|
        @nested_models << new_reduced_model
      end
    end
  end
  @nested_models
end

#nested_property?Boolean

Returns:

  • (Boolean)


98
99
100
# File 'lib/glimmer/data_binding/model_binding.rb', line 98

def nested_property?
  property_name_expression.to_s.match(/[.\[]/)
end

#nested_property_nameObject

Final nested property name e.g. property name expression “address.state” gives :state



88
89
90
# File 'lib/glimmer/data_binding/model_binding.rb', line 88

def nested_property_name
  nested_property_names.last
end

#nested_property_namesObject

All nested property names e.g. property name expression “address.state” gives [‘address’, ‘state’] If there are any indexed property names, this returns indexes as properties. e.g. property name expression “addresses.state” gives [‘addresses’, ‘[1]’, ‘state’]



82
83
84
# File 'lib/glimmer/data_binding/model_binding.rb', line 82

def nested_property_names
  @nested_property_names ||= Concurrent::Array.new(property_name_expression.split(/\[|\./).map {|pne| pne.end_with?(']') ? "[#{pne}" : pne }.reject {|pne| pne.empty? })
end

#nested_property_observers_for(observer) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/glimmer/data_binding/model_binding.rb', line 114

def nested_property_observers_for(observer)
  @nested_property_observers_collection ||= Concurrent::Hash.new
  unless @nested_property_observers_collection.has_key?(observer)
    @nested_property_observers_collection[observer] = nested_property_names.reduce(Concurrent::Hash.new) do |output, property_name|
      output.merge(
        property_name => Observer.proc do |new_value|
          # Ensure reattaching observers when a higher level nested property is updated (e.g. person.address changes reattaches person.address.street observer)
          add_observer(observer)
          converted_value = evaluate_property
          observer.call(converted_value).tap do
            apply_processor(@binding_options[:after_read], converted_value)
          end
        end
      )
    end
  end
  @nested_property_observers_collection[observer]
end

#observation_optionsObject



110
111
112
# File 'lib/glimmer/data_binding/model_binding.rb', line 110

def observation_options
  @binding_options.slice(:recursive)
end

#options_property_nameObject



230
231
232
# File 'lib/glimmer/data_binding/model_binding.rb', line 230

def options_property_name
  self.property_name + "_options"
end

#property_indexed?(property_expression) ⇒ Boolean

Returns:

  • (Boolean)


234
235
236
# File 'lib/glimmer/data_binding/model_binding.rb', line 234

def property_indexed?(property_expression)
  property_expression.to_s.start_with?('[')
end

#property_nameObject



74
75
76
# File 'lib/glimmer/data_binding/model_binding.rb', line 74

def property_name
  nested_property? ? nested_property_name : property_name_expression
end

#remove_observer(observer, extra_options = {}) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/glimmer/data_binding/model_binding.rb', line 153

def remove_observer(observer, extra_options = {})
  if computed?
    @computed_model_bindings.each do |computed_model_binding|
      computed_observer_for(observer).unobserve(computed_model_binding)
    end
    @computed_observer_collection.delete(observer)
  elsif nested_property?
    # No need to call remove_nested_observers(observer) (cleanup happens automatically indirectly when invoked through observer.unobserve(model_binding))
    nested_property_observers_for(observer).clear
  else
    observer.unobserve(model, property_name)
  end
end