motion-prime 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +8 -8
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile.lock +14 -11
  4. data/README.md +8 -11
  5. data/Rakefile +2 -1
  6. data/bin/prime.rb +47 -0
  7. data/doc/FAQ.md +1 -1
  8. data/files/app/app_delegate.rb +1 -1
  9. data/files/app/config/base.rb +8 -4
  10. data/files/app/screens/application_screen.rb +1 -1
  11. data/files/app/screens/sidebar_screen.rb +1 -1
  12. data/files/app/sections/sidebar/action.rb +1 -1
  13. data/files/app/sections/sidebar/table.rb +1 -1
  14. data/files/app/styles/sidebar.rb +5 -5
  15. data/motion-prime.gemspec +1 -0
  16. data/motion-prime/api_client.rb +81 -0
  17. data/motion-prime/app_delegate.rb +22 -5
  18. data/motion-prime/config/base.rb +5 -0
  19. data/motion-prime/core_ext/kernel.rb +5 -0
  20. data/motion-prime/elements/_field_dimensions_mixin.rb +43 -0
  21. data/motion-prime/elements/_text_dimensions_mixin.rb +39 -0
  22. data/motion-prime/elements/base.rb +40 -17
  23. data/motion-prime/elements/button.rb +20 -0
  24. data/motion-prime/elements/draw.rb +2 -2
  25. data/motion-prime/elements/draw/image.rb +4 -2
  26. data/motion-prime/elements/draw/label.rb +1 -1
  27. data/motion-prime/elements/error_message.rb +3 -16
  28. data/motion-prime/elements/label.rb +13 -2
  29. data/motion-prime/elements/text_field.rb +1 -0
  30. data/motion-prime/helpers/cell_section.rb +9 -0
  31. data/motion-prime/helpers/has_authorization.rb +4 -3
  32. data/motion-prime/helpers/has_normalizer.rb +20 -6
  33. data/motion-prime/helpers/has_search_bar.rb +19 -7
  34. data/motion-prime/helpers/has_style_chain_builder.rb +7 -0
  35. data/motion-prime/models/association.rb +22 -9
  36. data/motion-prime/models/association_collection.rb +54 -23
  37. data/motion-prime/models/bag.rb +13 -12
  38. data/motion-prime/models/base.rb +2 -0
  39. data/motion-prime/models/errors.rb +23 -14
  40. data/motion-prime/models/finder.rb +4 -1
  41. data/motion-prime/models/model.rb +25 -5
  42. data/motion-prime/models/store_extension.rb +1 -7
  43. data/motion-prime/models/sync.rb +75 -43
  44. data/motion-prime/mp.rb +4 -0
  45. data/motion-prime/screens/_base_mixin.rb +18 -12
  46. data/motion-prime/screens/_navigation_bar_mixin.rb +15 -6
  47. data/motion-prime/screens/_navigation_mixin.rb +15 -16
  48. data/motion-prime/screens/base_screen.rb +5 -1
  49. data/motion-prime/screens/sidebar_container_screen.rb +37 -22
  50. data/motion-prime/sections/base.rb +82 -16
  51. data/motion-prime/sections/form.rb +144 -26
  52. data/motion-prime/sections/form/base_field_section.rb +62 -29
  53. data/motion-prime/sections/form/base_header_section.rb +27 -0
  54. data/motion-prime/sections/form/date_field_section.rb +2 -17
  55. data/motion-prime/sections/form/password_field_section.rb +3 -17
  56. data/motion-prime/sections/form/select_field_section.rb +4 -35
  57. data/motion-prime/sections/form/string_field_section.rb +3 -29
  58. data/motion-prime/sections/form/submit_field_section.rb +1 -7
  59. data/motion-prime/sections/form/switch_field_section.rb +3 -23
  60. data/motion-prime/sections/form/text_field_section.rb +3 -33
  61. data/motion-prime/sections/form/text_with_button_field_section.rb +4 -40
  62. data/motion-prime/sections/tabbed.rb +25 -5
  63. data/motion-prime/sections/table.rb +86 -22
  64. data/motion-prime/sections/table/refresh_mixin.rb +3 -1
  65. data/motion-prime/styles/base.rb +7 -89
  66. data/motion-prime/styles/form.rb +116 -0
  67. data/motion-prime/support/dm_button.rb +32 -5
  68. data/motion-prime/support/dm_text_field.rb +31 -7
  69. data/motion-prime/support/dm_text_view.rb +6 -3
  70. data/motion-prime/support/dm_view_controller.rb +3 -3
  71. data/motion-prime/support/ui_search_bar_custom.rb +1 -1
  72. data/motion-prime/version.rb +1 -1
  73. data/motion-prime/views/layout.rb +18 -10
  74. data/motion-prime/views/styles.rb +19 -9
  75. data/motion-prime/views/view_builder.rb +18 -2
  76. data/motion-prime/views/view_styler.rb +59 -5
  77. data/spec/models/errors_spec.rb +3 -3
  78. data/travis.sh +3 -2
  79. metadata +28 -5
  80. data/motion-prime/elements/_text_height_mixin.rb +0 -17
  81. data/motion-prime/sections/form/table_field_section.rb +0 -51
@@ -1,6 +1,9 @@
1
1
  motion_require './table.rb'
2
+ motion_require '../helpers/has_style_chain_builder'
2
3
  module MotionPrime
3
4
  class FormSection < TableSection
5
+ include HasStyleChainBuilder
6
+
4
7
  # MotionPrime::FormSection is container for Field Sections.
5
8
  # Forms are located inside Screen and can contain multiple Field Sections.
6
9
  # On render, each field will be added to parent screen.
@@ -18,46 +21,100 @@ module MotionPrime
18
21
  #
19
22
 
20
23
  class_attribute :text_field_limits, :text_view_limits
21
- class_attribute :fields_options
22
- attr_accessor :fields, :field_indexes, :keyboard_visible
24
+ class_attribute :fields_options, :section_header_options
25
+ attr_accessor :fields, :field_indexes, :keyboard_visible, :rendered_views, :section_headers
23
26
 
24
27
  def table_data
25
- fields.values
28
+ if @groups_count == 1
29
+ fields.values
30
+ else
31
+ section_indexes = []
32
+ fields.inject([]) do |result, (key, field)|
33
+ section = self.class.fields_options[key][:group].to_i
34
+
35
+ section_indexes[section] ||= 0
36
+ result[section] ||= []
37
+ result[section][section_indexes[section]] = field
38
+ section_indexes[section] += 1
39
+ result
40
+ end
41
+ end
42
+ end
43
+
44
+ def form_styles
45
+ base_styles = [:base_form]
46
+ base_styles << :base_form_with_sections unless flat_data?
47
+ item_styles = [name.to_sym]
48
+ {common: base_styles, specific: item_styles}
49
+ end
50
+
51
+ def field_styles(field)
52
+ suffixes = [:field]
53
+ if field.is_a?(BaseFieldSection)
54
+ suffixes << field.class_name_without_kvo.demodulize.underscore.gsub(/\_section$/, '')
55
+ end
56
+
57
+ styles = {
58
+ common: build_styles_chain(form_styles[:common], suffixes),
59
+ specific: build_styles_chain(form_styles[:specific], suffixes)
60
+ }
61
+
62
+ if field.respond_to?(:container_styles) && field.container_styles.present?
63
+ styles[:specific] += Array.wrap(field.container_styles)
64
+ end
65
+ styles
66
+ end
67
+
68
+ def header_styles(header)
69
+ suffixes = [:header, :"#{header.name}_header"]
70
+ styles = {
71
+ common: build_styles_chain(form_styles[:common], suffixes),
72
+ specific: build_styles_chain(form_styles[:specific], suffixes)
73
+ }
74
+
75
+ if header.respond_to?(:container_styles) && header.container_styles.present?
76
+ styles[:specific] += Array.wrap(header.container_styles)
77
+ end
78
+ styles
26
79
  end
27
80
 
28
81
  def render_table
29
82
  init_form_fields
30
- set_data_stamp(self.field_indexes.values)
31
- self.table_view = screen.table_view(
32
- styles: [:base_form, name.to_sym], delegate: self, dataSource: self
33
- ).view
83
+ reset_data_stamps
84
+ options = {
85
+ styles: form_styles.values.flatten,
86
+ delegate: self,
87
+ dataSource: self,
88
+ style: (UITableViewStyleGrouped unless flat_data?)}
89
+ self.table_element = screen.table_view(options)
34
90
  end
35
91
 
36
92
  def render_cell(index, table)
37
- item = data[index.row]
38
- styles = [:base_form_field, :"#{name}_field"]
39
- if item.respond_to?(:container_styles) && item.container_styles.present?
40
- styles += Array.wrap(item.container_styles)
41
- end
42
- screen.table_view_cell styles: styles, reuse_identifier: cell_name(table, index) do |cell_element|
43
- item.cell_element = cell_element if item.respond_to?(:cell_element)
44
- item.render(to: screen)
93
+ field = rows_for_section(index.section)[index.row]
94
+ screen.table_view_cell styles: field_styles(field).values.flatten, reuse_identifier: cell_name(table, index), parent_view: table_view do |cell_view|
95
+ field.cell_view = cell_view if field.respond_to?(:cell_view)
96
+ field.render(to: screen)
45
97
  end
46
98
  end
47
99
 
48
100
  def reload_cell(section)
49
101
  field = section.name.to_sym
50
- path = table_view.indexPathForRowAtPoint(section.cell.center) # do not use indexPathForCell here as field may be invisibe
102
+ index = field_indexes[field].split('_').map(&:to_i)
103
+ path = NSIndexPath.indexPathForRow(index.last, inSection: index.first)
51
104
  table_view.beginUpdates
52
- section.cell.removeFromSuperview
105
+ section.cell.try(:removeFromSuperview)
53
106
 
54
107
  fields[field] = load_field(self.class.fields_options[field])
55
108
  @data = nil
56
- set_data_stamp([field_indexes[field]])
109
+ set_data_stamp(field_indexes[field])
57
110
  table_view.reloadRowsAtIndexPaths([path], withRowAnimation: UITableViewRowAnimationNone)
58
111
  table_view.endUpdates
59
112
  end
60
113
 
114
+ def reset_data_stamps
115
+ set_data_stamp(self.field_indexes.values)
116
+ end
117
+
61
118
  # Returns element based on field name and element name
62
119
  #
63
120
  # Examples:
@@ -89,6 +146,13 @@ module MotionPrime
89
146
  fields.to_hash
90
147
  end
91
148
 
149
+ def register_elements_from_section(section)
150
+ self.rendered_views ||= {}
151
+ section.elements.values.each do |element|
152
+ self.rendered_views[element.view] = {element: element, section: section}
153
+ end
154
+ end
155
+
92
156
  # Set focus on field cell
93
157
  #
94
158
  # Examples:
@@ -98,7 +162,7 @@ module MotionPrime
98
162
  # @return MotionPrime::BaseFieldSection field
99
163
  def focus_on(field_name, animated = true)
100
164
  # unfocus other field
101
- data.each do |item|
165
+ data.flatten.each do |item|
102
166
  item.blur
103
167
  end
104
168
  # focus on field
@@ -117,6 +181,19 @@ module MotionPrime
117
181
  self.keyboard_visible = false
118
182
  end
119
183
 
184
+ def keyboard_will_show
185
+ return if table_view.contentSize.height + table_view.top <= UIScreen.mainScreen.bounds.size.height
186
+ current_inset = table_view.contentInset
187
+ current_inset.bottom = KEYBOARD_HEIGHT_PORTRAIT + (self.table_element.computed_options[:bottom_content_offset] || 0)
188
+ table_view.contentInset = current_inset
189
+ end
190
+
191
+ def keyboard_will_hide
192
+ current_inset = table_view.contentInset
193
+ current_inset.bottom = self.table_element.computed_options[:bottom_content_offset] || 0
194
+ table_view.contentInset = current_inset
195
+ end
196
+
120
197
  # ALIASES
121
198
  def on_input_change(text_field); end
122
199
  def on_input_edit(text_field); end
@@ -159,19 +236,57 @@ module MotionPrime
159
236
  klass.new(field.merge(form: self))
160
237
  end
161
238
 
162
- def render_field?(name)
163
- true
239
+ def render_field?(name, options)
240
+ condition = options.delete(:if)
241
+ if condition.nil?
242
+ true
243
+ elsif condition.is_a?(Proc)
244
+ self.instance_eval(&condition)
245
+ else
246
+ condition
247
+ end
248
+ end
249
+
250
+ def render_header(section)
251
+ return unless options = self.class.section_header_options.try(:[], section)
252
+ self.section_headers[section] ||= BaseHeaderSection.new(options.merge(form: self))
253
+ end
254
+
255
+ def header_for_section(section)
256
+ self.section_headers ||= []
257
+ self.section_headers[section] || render_header(section)
258
+ end
259
+
260
+ def tableView(table, viewForHeaderInSection: section)
261
+ return unless header = header_for_section(section)
262
+ wrapper = MotionPrime::BaseElement.factory(:view, styles: header_styles(header).values.flatten, parent_view: table_view)
263
+ wrapper.render(to: screen) do |cell_view|
264
+ header.cell_view = cell_view if header.respond_to?(:cell_view)
265
+ header.render(to: screen)
266
+ end
267
+ end
268
+
269
+ def tableView(table, heightForHeaderInSection: section)
270
+ header_for_section(section).try(:container_height) || 0
164
271
  end
165
272
 
166
273
  class << self
167
- def field(name, options = {})
274
+ def field(name, options = {}, &block)
168
275
  options[:name] = name
169
276
  options[:type] ||= :string
277
+ options[:block] = block
170
278
  self.fields_options ||= {}
171
279
  self.fields_options[name] = options
172
280
  self.fields_options[name]
173
281
  end
174
282
 
283
+ def group_header(name, options)
284
+ options[:name] = name
285
+ self.section_header_options ||= []
286
+ section = options.delete(:id)
287
+ self.section_header_options[section] = options
288
+ end
289
+
175
290
  def limit_text_field_length(name, limit)
176
291
  self.text_field_limits ||= {}
177
292
  self.text_field_limits[name] = limit
@@ -186,12 +301,15 @@ module MotionPrime
186
301
  def init_form_fields
187
302
  self.fields = {}
188
303
  self.field_indexes = {}
189
- index = 0
304
+ section_indexes = []
190
305
  (self.class.fields_options || []).each do |key, field|
191
- next unless render_field?(key)
306
+ next unless render_field?(key, field)
307
+ section_id = field[:group].to_i
308
+ section_indexes[section_id] ||= 0
309
+ @groups_count = [@groups_count || 1, section_id + 1].max
192
310
  self.fields[key] = load_field(field)
193
- self.field_indexes[key] = index
194
- index += 1
311
+ self.field_indexes[key] = "#{section_id}_#{section_indexes[section_id]}"
312
+ section_indexes[section_id] += 1
195
313
  end
196
314
  end
197
315
  end
@@ -1,5 +1,6 @@
1
1
  module MotionPrime
2
2
  class BaseFieldSection < BaseSection
3
+ include CellSection
3
4
  include BW::KVO
4
5
  attr_accessor :form
5
6
 
@@ -7,18 +8,23 @@ module MotionPrime
7
8
 
8
9
  def initialize(options = {})
9
10
  @form = options.delete(:form)
10
- if options[:observe_errors_for]
11
- @observe_errors_for = self.send(:instance_eval, &options.delete(:observe_errors_for))
12
- end
11
+ @errors_observer_options = normalize_options(options.delete(:observe_errors).clone, self) if options[:observe_errors]
13
12
  super
14
- @container_options = options.delete(:container)
15
13
  observe_model_errors
16
14
  end
17
15
 
16
+ def style_options
17
+ @style_options ||= Styles.for(section_styles.values.flatten)
18
+ end
19
+
20
+ def section_styles
21
+ form.try(:field_styles, self)
22
+ end
23
+
18
24
  def render_element?(element_name)
19
25
  case element_name.to_sym
20
26
  when :error_message
21
- @observe_errors_for && @observe_errors_for.errors[name].present?
27
+ has_errors?
22
28
  when :label
23
29
  not @options[:label] === false
24
30
  else true
@@ -27,37 +33,28 @@ module MotionPrime
27
33
 
28
34
  def on_section_render
29
35
  @status_for_updated = :rendered
36
+ form.register_elements_from_section(self)
30
37
  end
31
38
 
32
39
  def observe_model_errors
33
- return unless @observe_errors_for
34
- observe @observe_errors_for.errors, name do |old_value, new_value|
35
- if @status_for_updated == :rendered
36
- clear_observer_and_reload
37
- else
38
- create_elements
39
- form.table_view.reloadData
40
+ return unless observing_errors?
41
+ errors_observer_fields.each do |field|
42
+ observe observing_errors_for.errors, observing_errors_for.errors.unique_key(field) do |old_value, new_value|
43
+ next if old_value == new_value
44
+ if @status_for_updated == :rendered
45
+ reload_section
46
+ else
47
+ load_section
48
+ form.table_view.reloadData
49
+ end
40
50
  end
41
51
  end
42
52
  end
43
53
 
44
- def clear_observer_and_reload
45
- unobserve @observe_errors_for.errors, name
46
- reload_section
47
- end
48
-
49
- def build_options_for_element(opts)
50
- super.merge(observe_errors_for: @observe_errors_for)
51
- end
52
-
53
54
  def form_name
54
55
  form.name
55
56
  end
56
57
 
57
- def container_options
58
- @container_options || super
59
- end
60
-
61
58
  def focus(begin_editing = true)
62
59
  # scroll to cell
63
60
  path = form.table_view.indexPathForCell cell
@@ -72,7 +69,7 @@ module MotionPrime
72
69
  end
73
70
  self
74
71
  rescue
75
- puts "can't focus on element #{self.class.name}"
72
+ puts "can't focus on element #{self.class_name_without_kvo}"
76
73
  end
77
74
 
78
75
  def blur
@@ -83,7 +80,7 @@ module MotionPrime
83
80
  end
84
81
  self
85
82
  rescue
86
- puts "can't blur on element #{self.class.name}"
83
+ puts "can't blur on element #{self.class_name_without_kvo}"
87
84
  end
88
85
 
89
86
  def bind_text_input
@@ -94,13 +91,49 @@ module MotionPrime
94
91
  view(:input).delegate = self.form
95
92
  end
96
93
 
94
+ def observing_errors?
95
+ @errors_observer_options.present?
96
+ end
97
+
98
+ def has_errors?
99
+ return false unless observing_errors?
100
+ errors_observer_fields.any? do |field|
101
+ observing_errors_for.errors[field].present?
102
+ end
103
+ end
104
+
105
+ def errors_observer_fields
106
+ @errors_observer_fields ||= begin
107
+ fields = Array.wrap(@errors_observer_options[:fields])
108
+ fields << name if fields.empty?
109
+ fields.uniq
110
+ end
111
+ end
112
+
113
+ def observing_errors_for
114
+ @errors_observer_options[:resource]
115
+ end
116
+
117
+ def all_errors
118
+ return [] unless observing_errors?
119
+
120
+ errors_observer_fields.map do |field|
121
+ observing_errors_for.errors[field]
122
+ end
123
+ end
124
+
97
125
  def reload_section
126
+ errors_observer_fields.each do |field|
127
+ unobserve observing_errors_for.errors, observing_errors_for.errors.unique_key(field)
128
+ end if observing_errors?
98
129
  form.reload_cell(self)
99
130
  end
100
131
 
101
132
  def container_height
102
- error_height = element(:error_message).try(:content_height)
103
- super + error_height.to_i
133
+ return 0 if container_options[:hidden]
134
+ element = element(:error_message)
135
+ error_height = element ? element.content_height + 5 : 0
136
+ super + error_height
104
137
  end
105
138
  end
106
139
  end
@@ -0,0 +1,27 @@
1
+ module MotionPrime
2
+ class BaseHeaderSection < BaseSection
3
+ include CellSection
4
+ DEFAULT_HEADER_HEIGHT = 20
5
+
6
+ element :title, text: proc { @options[:title] }
7
+
8
+ attr_accessor :form
9
+
10
+ def initialize(options = {})
11
+ @form = options[:form]
12
+ super
13
+ end
14
+
15
+ def style_options
16
+ @style_options ||= Styles.for(section_styles.values.flatten)
17
+ end
18
+
19
+ def section_styles
20
+ form.header_styles(self)
21
+ end
22
+
23
+ def container_height
24
+ container_options[:height] || DEFAULT_HEADER_HEIGHT
25
+ end
26
+ end
27
+ end
@@ -2,24 +2,9 @@ module MotionPrime
2
2
  class DateFieldSection < BaseFieldSection
3
3
  container height: 190
4
4
  element :label, type: :label do
5
- {
6
- styles: [
7
- :base_field_label,
8
- :base_date_picker_field_label,
9
- :"#{form_name}_field_label",
10
- :"#{form_name}_#{name}_field_label"
11
- ]
12
- }.merge(options[:label] || {})
13
- end
14
- element :date_picker, type: :date_picker do
15
- {
16
- styles: [
17
- :base_date_picker,
18
- :"#{form_name}_date_picker",
19
- :"#{form_name}_#{name}_date_picker"
20
- ]
21
- }
5
+ options[:label] || {}
22
6
  end
7
+ element :date_picker, type: :date_picker
23
8
 
24
9
  after_render :bind_date_picker
25
10