formotion 0.5.1 → 1.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 (85) hide show
  1. data/CHANGELOG.md +24 -0
  2. data/LIST_OF_ROW_TYPES.md +1 -1
  3. data/Rakefile +20 -5
  4. data/app/app_delegate.rb +89 -17
  5. data/examples/FormModel/.gitignore +5 -0
  6. data/examples/FormModel/Rakefile +9 -0
  7. data/examples/FormModel/app/app_delegate.rb +11 -0
  8. data/examples/FormModel/app/user.rb +18 -0
  9. data/examples/FormModel/app/users_controller.rb +52 -0
  10. data/examples/FormModel/spec/main_spec.rb +9 -0
  11. data/examples/KitchenSink/app/app_delegate.rb +2 -2
  12. data/gh-pages/assets/css/bootstrap-responsive.css +815 -0
  13. data/gh-pages/assets/css/bootstrap-responsive.min.css +9 -0
  14. data/gh-pages/assets/css/bootstrap.css +4983 -0
  15. data/gh-pages/assets/css/bootstrap.min.css +9 -0
  16. data/gh-pages/assets/css/formotion.css +117 -0
  17. data/gh-pages/assets/css/pygments.css +62 -0
  18. data/gh-pages/assets/img/glyphicons-halflings-white.png +0 -0
  19. data/gh-pages/assets/img/glyphicons-halflings.png +0 -0
  20. data/gh-pages/assets/js/bootstrap-alert.js +90 -0
  21. data/gh-pages/assets/js/bootstrap-button.js +96 -0
  22. data/gh-pages/assets/js/bootstrap-carousel.js +169 -0
  23. data/gh-pages/assets/js/bootstrap-collapse.js +157 -0
  24. data/gh-pages/assets/js/bootstrap-dropdown.js +100 -0
  25. data/gh-pages/assets/js/bootstrap-modal.js +218 -0
  26. data/gh-pages/assets/js/bootstrap-popover.js +98 -0
  27. data/gh-pages/assets/js/bootstrap-scrollspy.js +151 -0
  28. data/gh-pages/assets/js/bootstrap-tab.js +135 -0
  29. data/gh-pages/assets/js/bootstrap-tooltip.js +275 -0
  30. data/gh-pages/assets/js/bootstrap-transition.js +61 -0
  31. data/gh-pages/assets/js/bootstrap-typeahead.js +285 -0
  32. data/gh-pages/assets/js/bootstrap.js +1825 -0
  33. data/gh-pages/assets/js/bootstrap.min.js +6 -0
  34. data/gh-pages/index.html +205 -0
  35. data/lib/formotion.rb +15 -2
  36. data/lib/formotion/base.rb +5 -5
  37. data/lib/formotion/{form_controller.rb → controller/form_controller.rb} +22 -3
  38. data/lib/formotion/controller/formable_controller.rb +21 -0
  39. data/lib/formotion/form/form.rb +31 -8
  40. data/lib/formotion/form/form_delegate.rb +26 -3
  41. data/lib/formotion/model/formable.rb +78 -0
  42. data/lib/formotion/row/row.rb +54 -21
  43. data/lib/formotion/row/row_cell_builder.rb +1 -1
  44. data/lib/formotion/row_type/back_row.rb +11 -0
  45. data/lib/formotion/row_type/base.rb +42 -1
  46. data/lib/formotion/row_type/button.rb +30 -0
  47. data/lib/formotion/row_type/check_row.rb +9 -5
  48. data/lib/formotion/row_type/date_row.rb +5 -0
  49. data/lib/formotion/row_type/edit_row.rb +14 -0
  50. data/lib/formotion/row_type/image_row.rb +7 -7
  51. data/lib/formotion/row_type/options_row.rb +18 -8
  52. data/lib/formotion/row_type/slider_row.rb +14 -5
  53. data/lib/formotion/row_type/string_row.rb +17 -5
  54. data/lib/formotion/row_type/subform_row.rb +16 -0
  55. data/lib/formotion/row_type/submit_row.rb +1 -24
  56. data/lib/formotion/row_type/switch_row.rb +10 -2
  57. data/lib/formotion/row_type/template_row.rb +77 -0
  58. data/lib/formotion/row_type/text_row.rb +12 -4
  59. data/lib/formotion/section/section.rb +9 -1
  60. data/lib/formotion/version.rb +1 -1
  61. data/spec/form_spec.rb +24 -0
  62. data/spec/formable_spec.rb +100 -0
  63. data/spec/functional/formable_controller_spec.rb +47 -0
  64. data/spec/functional/image_row_spec.rb +2 -2
  65. data/spec/functional/subform_row.rb +33 -0
  66. data/spec/functional/template_row_spec.rb +57 -0
  67. data/spec/helpers/row_test_helpers.rb +26 -0
  68. data/spec/row_type/back_spec.rb +31 -0
  69. data/spec/row_type/check_spec.rb +9 -9
  70. data/spec/row_type/date_spec.rb +3 -10
  71. data/spec/row_type/email_spec.rb +1 -9
  72. data/spec/row_type/image_spec.rb +3 -12
  73. data/spec/row_type/number_spec.rb +1 -9
  74. data/spec/row_type/options_spec.rb +18 -10
  75. data/spec/row_type/phone_spec.rb +1 -9
  76. data/spec/row_type/picker_spec.rb +2 -11
  77. data/spec/row_type/slider_spec.rb +11 -10
  78. data/spec/row_type/static_spec.rb +1 -9
  79. data/spec/row_type/string_spec.rb +13 -9
  80. data/spec/row_type/subform_spec.rb +47 -0
  81. data/spec/row_type/submit_spec.rb +1 -9
  82. data/spec/row_type/switch_spec.rb +9 -9
  83. data/spec/row_type/template_spec.rb +14 -0
  84. data/spec/row_type/text_spec.rb +9 -9
  85. metadata +58 -6
@@ -0,0 +1,78 @@
1
+ module Formotion
2
+ module Formable
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ # Relates a property to a RowType.
9
+ # @param property is the name of the attribute to KVO
10
+ # @param row_type is the Formotion::RowType to use for that attribute
11
+ # @param options are the extra options for this model. Keys can include
12
+ # any usual Formotion::Row keys to override, plus :transform, which is
13
+ # a single-argument lambda for transforming the row's string value before
14
+ # it's synced to your model.
15
+ # EX
16
+ # form_property :my_title => :string
17
+ # form_property :my_date => :date, :transform => lambda { |value| some_function(date) }
18
+ def form_property(property, row_type, options = {})
19
+ self.form_properties << { property: property, row_type: row_type}.merge(options)
20
+ end
21
+
22
+ def form_properties
23
+ @form_properties ||= []
24
+ end
25
+
26
+ # Sets the top bar title for this model
27
+ # EX
28
+ # form_title "Some Settings"
29
+ def form_title(title = -1)
30
+ @form_title = title if title != -1
31
+ @form_title
32
+ end
33
+ end
34
+
35
+ # Creates a Formotion::Form out of the model
36
+ def to_form
37
+ rows = self.class.form_properties.collect { |options|
38
+ {
39
+ title: options[:property].capitalize,
40
+ key: options[:property],
41
+ type: options[:row_type],
42
+ value: self.send(options[:property])
43
+ }.merge(options)
44
+ }
45
+ form_hash = {
46
+ title: self.class.form_title || self.class.to_s.capitalize,
47
+ sections: [{
48
+ rows: rows
49
+ }]
50
+ }
51
+
52
+ form = Formotion::Form.new(form_hash)
53
+ form.on_submit do
54
+ self.on_submit
55
+ end
56
+
57
+ # Use the :transform lambdas passed in form_property
58
+ form.sections.first.rows.each_with_index { |row, index|
59
+ row.instance_variable_set("@formable_options", self.class.form_properties[index])
60
+ if self.class.form_properties[index][:transform]
61
+ row.class.send(:alias_method, :old_value_setter, :value=)
62
+ row.instance_eval do
63
+ def value=(value)
64
+ old_value_setter(@formable_options[:transform].call(value))
65
+ end
66
+ end
67
+ end
68
+ }
69
+
70
+ form
71
+ end
72
+
73
+ # what happens when the form is submitted?
74
+ def on_submit
75
+ p "need to implement on_submit in your Formable model #{self.class.to_s}"
76
+ end
77
+ end
78
+ end
@@ -16,6 +16,8 @@ module Formotion
16
16
  # Stores possible formatting information (used by date pickers, etc)
17
17
  # if :type == :date, accepts values in [:short, :medium, :long, :full]
18
18
  :format,
19
+ # alternative title for row (only used in EditRow for now)
20
+ :alt_title,
19
21
 
20
22
  # The following apply only to text-input fields
21
23
 
@@ -38,8 +40,8 @@ module Formotion
38
40
  :clear_button,
39
41
  # row height as integer; used for heightForRowAtIndexPath
40
42
  # EX 200
41
- # DEFAULT is nil, which is used as the tableView.rowHeight
42
- :rowHeight,
43
+ # DEFAULT is nil, which is used as the tableView.row_height
44
+ :row_height,
43
45
  # range used for slider min and max value
44
46
  # EX (1..100)
45
47
  # DEFAULT is (1..10)
@@ -48,11 +50,30 @@ module Formotion
48
50
  # EX ['free', 'pro']
49
51
  # DEFAULT is []
50
52
  :items,
53
+ # A hash for a Form used for subforms
54
+ # DEFAULT is nil
55
+ :subform,
56
+ # A hash for a Row used for templates
57
+ # DEFAULT is nil
58
+ :template,
59
+ # Indents row when set to true
60
+ # DEFAULT is false
61
+ :indented,
62
+ # Shows a delete sign next to the row
63
+ # DEFAULT is false
64
+ :deletable,
65
+ # When a row is deleted, actually remove the row from UI
66
+ # instead of just nil'ing the value.
67
+ # DEFAULT is false EXCEPT for template-generated rows
68
+ :remove_on_delete
51
69
  ]
52
70
  PROPERTIES.each {|prop|
53
71
  attr_accessor prop
54
72
  }
55
- BOOLEAN_PROPERTIES = [:secure]
73
+ BOOLEAN_PROPERTIES = [:secure, :indented, :deletable, :remove_on_delete]
74
+ BOOLEAN_PROPERTIES.each { |prop|
75
+ alias_method "#{prop}?", prop
76
+ }
56
77
  SERIALIZE_PROPERTIES = PROPERTIES
57
78
 
58
79
  # Reference to the row's section
@@ -78,31 +99,27 @@ module Formotion
78
99
  # starts editing #text_field.
79
100
  attr_accessor :on_begin_callback
80
101
 
81
- # row type object
102
+ # RowType object
82
103
  attr_accessor :object
83
104
 
105
+ # Owning template row, if applicable
106
+ attr_accessor :template_parent
107
+
84
108
  def initialize(params = {})
85
109
  super
86
110
 
87
- BOOLEAN_PROPERTIES.each {|prop|
111
+ BOOLEAN_PROPERTIES.each { |prop|
88
112
  Formotion::Conditions.assert_nil_or_boolean(self.send(prop))
89
113
  }
90
114
  end
91
115
 
92
- # Makes all ::BOOLEAN_PROPERTIES queriable with an appended ?
93
- # these should be done with alias_method but there's currently a bug
94
- # in RM which messes up attr_accessors with alias_method
95
- # EX
96
- # row.secure?
97
- # => true
98
- # row.checkable?
99
- # => nil
100
- def method_missing(method, *args, &block)
101
- boolean_method = (method.to_s[0..-2]).to_sym
102
- if BOOLEAN_PROPERTIES.member? boolean_method
103
- return self.send(boolean_method)
116
+ # called after section and index have been assigned
117
+ def after_create
118
+ if self.type == :template and (self.value && self.value.any?)
119
+ self.value.each do |value|
120
+ new_row = self.object.build_new_row({:value => value})
121
+ end
104
122
  end
105
- super
106
123
  end
107
124
 
108
125
  #########################
@@ -117,7 +134,7 @@ module Formotion
117
134
  end
118
135
 
119
136
  def reuse_identifier
120
- @reuse_identifier || "SECTION_#{self.section.index}_ROW_#{self.index}"
137
+ @reuse_identifier || "Formotion_#{self.object_id}"
121
138
  end
122
139
 
123
140
  def next_row
@@ -139,8 +156,8 @@ module Formotion
139
156
  nil
140
157
  end
141
158
 
142
- def submit_button?
143
- object.submit_button?
159
+ def button?
160
+ object.button?
144
161
  end
145
162
 
146
163
  #########################
@@ -194,6 +211,22 @@ module Formotion
194
211
  super
195
212
  end
196
213
 
214
+ def subform=(subform)
215
+ @subform = subform
216
+ # enables you do to row.subform.to_form
217
+ @subform.instance_eval do
218
+ def to_form
219
+ return @hash_subform if @hash_subform
220
+ if self.is_a? Hash
221
+ @hash_subform = Formotion::Form.new(self)
222
+ elsif not self.is_a? Formotion::Form
223
+ raise Formotion::InvalidClassError, "Attempted subform = '#{self.inspect}' should be of type Formotion::Form or Hash"
224
+ end
225
+ @hash_subform ||= self
226
+ end
227
+ end
228
+ end
229
+
197
230
  private
198
231
  def const_int_get(base, value)
199
232
  return value if value.is_a? Integer
@@ -15,7 +15,7 @@ module Formotion
15
15
 
16
16
  cell = UITableViewCell.alloc.initWithStyle(row.object.cell_style, reuseIdentifier:row.reuse_identifier)
17
17
 
18
- cell.accessoryType = UITableViewCellAccessoryNone
18
+ cell.accessoryType = cell.editingAccessoryType = UITableViewCellAccessoryNone
19
19
  cell.textLabel.text = row.title
20
20
  cell.detailTextLabel.text = row.subtitle
21
21
 
@@ -0,0 +1,11 @@
1
+ module Formotion
2
+ module RowType
3
+ class BackRow < Button
4
+
5
+ def on_select(tableView, tableViewDelegate)
6
+ row.form.controller.pop_subform
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -3,6 +3,8 @@ module Formotion
3
3
  class Base
4
4
  attr_accessor :row, :tableView
5
5
 
6
+ FIELD_BUFFER = Device.iphone? ? 20 : 64
7
+
6
8
  def tableView
7
9
  @tableView ||= self.row.form.table
8
10
  end
@@ -11,7 +13,7 @@ module Formotion
11
13
  @row = row
12
14
  end
13
15
 
14
- def submit_button?
16
+ def button?
15
17
  false
16
18
  end
17
19
 
@@ -20,6 +22,16 @@ module Formotion
20
22
  UITableViewCellStyleSubtitle
21
23
  end
22
24
 
25
+ # Sets the UITableViewCellEditingStyle
26
+ def cellEditingStyle
27
+ row.deletable? ? UITableViewCellEditingStyleDelete : UITableViewCellEditingStyleNone
28
+ end
29
+
30
+ # Indents row while editing
31
+ def indentWhileEditing?
32
+ row.indented?
33
+ end
34
+
23
35
  # builder method for row cell specific implementation
24
36
  def build_cell(cell)
25
37
  # implement in row class
@@ -36,6 +48,35 @@ module Formotion
36
48
  def on_select(tableView, tableViewDelegate)
37
49
  # implement in row class
38
50
  end
51
+
52
+ # called when the delete editing style was triggered tableView:commitEditingStyle:forRowAtIndexPath:
53
+ def on_delete(tableView, tableViewDelegate)
54
+ if row.remove_on_delete?
55
+ row.section.rows.delete_at(row.index)
56
+ row.section.refresh_row_indexes
57
+ delete_row
58
+ else
59
+ row.value = nil
60
+ self.tableView.reloadData
61
+ end
62
+ end
63
+
64
+ def delete_row
65
+ tableView.beginUpdates
66
+ tableView.deleteRowsAtIndexPaths [row.index_path], withRowAnimation:UITableViewRowAnimationBottom
67
+ tableView.endUpdates
68
+ end
69
+
70
+ def break_with_semaphore(&block)
71
+ return if @semaphore
72
+ with_semaphore(&block)
73
+ end
74
+
75
+ def with_semaphore(&block)
76
+ @semaphore = true
77
+ block.call
78
+ @semaphore = false
79
+ end
39
80
  end
40
81
  end
41
82
  end
@@ -0,0 +1,30 @@
1
+ module Formotion
2
+ module RowType
3
+ class Button < Base
4
+
5
+ def button?
6
+ true
7
+ end
8
+
9
+ # Does a clever little trick to override #layoutSubviews
10
+ # for just this one UITableViewCell object, in order to
11
+ # center it's labels horizontally.
12
+ def build_cell(cell)
13
+ cell.swizzle(:layoutSubviews) do
14
+ def layoutSubviews
15
+ old_layoutSubviews
16
+
17
+ center = lambda {|frame, dimen|
18
+ ((self.frame.size.send(dimen) - frame.size.send(dimen)) / 2.0)
19
+ }
20
+
21
+ self.textLabel.center = CGPointMake(self.frame.size.width / 2 - (FIELD_BUFFER / 2), self.textLabel.center.y)
22
+ self.detailTextLabel.center = CGPointMake(self.frame.size.width / 2 - (FIELD_BUFFER / 2), self.detailTextLabel.center.y)
23
+ end
24
+ end
25
+ nil
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -1,13 +1,21 @@
1
1
  module Formotion
2
2
  module RowType
3
3
  class CheckRow < Base
4
+ include BW::KVO
5
+
6
+ def update_cell_value(cell)
7
+ cell.accessoryType = cell.editingAccessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone
8
+ end
4
9
 
5
10
  # This is actually called whenever again cell is checked/unchecked
6
11
  # in the UITableViewDelegate callbacks. So (for now) don't
7
12
  # instantiate long-lived objects in them.
8
13
  # Maybe that logic should be moved elsewhere?
9
14
  def build_cell(cell)
10
- cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone
15
+ update_cell_value(cell)
16
+ observe(self.row, "value") do |old_value, new_value|
17
+ update_cell_value(cell)
18
+ end
11
19
  nil
12
20
  end
13
21
 
@@ -15,13 +23,9 @@ module Formotion
15
23
  if row.section.select_one and !row.value
16
24
  row.section.rows.each do |other_row|
17
25
  other_row.value = (other_row == row)
18
-
19
- cell = tableView.cellForRowAtIndexPath(other_row.index_path)
20
- other_row.object.build_cell(cell) if cell
21
26
  end
22
27
  elsif !row.section.select_one
23
28
  row.value = !row.value
24
- build_cell(tableView.cellForRowAtIndexPath(row.index_path))
25
29
  end
26
30
  end
27
31
 
@@ -56,6 +56,11 @@ module Formotion
56
56
  picker
57
57
  end
58
58
  end
59
+
60
+ # Used when row.value changes
61
+ def update_text_field(new_value)
62
+ self.row.text_field.text = self.formatted_value
63
+ end
59
64
  end
60
65
  end
61
66
  end
@@ -0,0 +1,14 @@
1
+ module Formotion
2
+ module RowType
3
+ class EditRow < Button
4
+ def on_select(tableView, tableViewDelegate)
5
+ was_editing = tableView.isEditing
6
+ if row.alt_title
7
+ new_title = !was_editing ? row.alt_title : row.title
8
+ tableView.cellForRowAtIndexPath(row.index_path).textLabel.text = new_title
9
+ end
10
+ tableView.setEditing(!was_editing, animated: true)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -11,10 +11,10 @@ module Formotion
11
11
  observe(self.row, "value") do |old_value, new_value|
12
12
  @image_view.image = new_value
13
13
  if new_value
14
- self.row.rowHeight = 200
15
- cell.accessoryView = nil
14
+ self.row.row_height = 200
15
+ cell.accessoryView = cell.editingAccessoryView = nil
16
16
  else
17
- self.row.rowHeight = 44
17
+ self.row.row_height = 44
18
18
  add_plus_accessory(cell)
19
19
  end
20
20
  row.form.reload_data
@@ -36,9 +36,9 @@ module Formotion
36
36
 
37
37
  field_frame = formotion_field.frame
38
38
  field_frame.origin.y = 10
39
- field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
40
- field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
41
- field_frame.size.height = self.frame.size.height - 20
39
+ field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + FIELD_BUFFER
40
+ field_frame.size.width = self.frame.size.width - field_frame.origin.x - FIELD_BUFFER
41
+ field_frame.size.height = self.frame.size.height - FIELD_BUFFER
42
42
  formotion_field.frame = field_frame
43
43
  end
44
44
  end
@@ -92,7 +92,7 @@ module Formotion
92
92
  end
93
93
  button
94
94
  end
95
- cell.accessoryView = @add_button
95
+ cell.accessoryView = cell.editingAccessoryView = @add_button
96
96
  end
97
97
  end
98
98
  end
@@ -1,18 +1,28 @@
1
1
  module Formotion
2
2
  module RowType
3
3
  class OptionsRow < Base
4
-
5
- SLIDER_VIEW_TAG = 1200
4
+ include BW::KVO
6
5
 
7
6
  def build_cell(cell)
8
7
  cell.selectionStyle = UITableViewCellSelectionStyleNone
9
- slideView = UISegmentedControl.alloc.initWithItems(row.items || [])
10
- slideView.selectedSegmentIndex = row.items.index(row.value) if row.value
11
- slideView.segmentedControlStyle = UISegmentedControlStyleBar
12
- cell.accessoryView = slideView
8
+ segmentedControl = UISegmentedControl.alloc.initWithItems(row.items || [])
9
+ segmentedControl.selectedSegmentIndex = row.items.index(row.value) if row.value
10
+ segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar
11
+ cell.accessoryView = cell.editingAccessoryView = segmentedControl
13
12
 
14
- slideView.when(UIControlEventValueChanged) do
15
- row.value = row.items[slideView.selectedSegmentIndex]
13
+ segmentedControl.when(UIControlEventValueChanged) do
14
+ break_with_semaphore do
15
+ row.value = row.items[segmentedControl.selectedSegmentIndex]
16
+ end
17
+ end
18
+ observe(self.row, "value") do |old_value, new_value|
19
+ break_with_semaphore do
20
+ if row.value
21
+ segmentedControl.selectedSegmentIndex = row.items.index(row.value)
22
+ else
23
+ segmentedControl.selectedSegmentIndex = UISegmentedControlNoSegment
24
+ end
25
+ end
16
26
  end
17
27
 
18
28
  nil