motion-prime 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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