formotion 1.2 → 1.3

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.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 1.3 - March 25, 2013
2
+
3
+ ### Features
4
+
5
+ - Added `:currency` row type, which automatically presents an entered number into the current locale's format (i.e. "$4,003.45" or "€ 3.004,35")
6
+
7
+ - Added `on_delete` callback for when a `Row` is deleted
8
+
9
+ - Correctly handle `on_tap` for `Row` objects, regardless of whether or not they are `:button`s.
10
+
11
+ ### Bug Fixes
12
+
13
+ - Fixed a crash that occured when rapidly serializing a form.
14
+
15
+ - Template rows are now persisted correctly.
16
+
17
+ - `ImageRow`s which are disabled will not show the "plus" icon
18
+
1
19
  ## 1.2 - January 3, 2013
2
20
 
3
21
  ### Features
data/Gemfile CHANGED
@@ -1,6 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'bubble-wrap', ">=1.1.4"
4
+ gem 'guard-motion'
5
+ gem 'rb-fsevent'
4
6
 
5
7
  # Specify your gem's dependencies in motion-settings.gemspec
6
8
  gemspec
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'motion' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/[^/]+/(.+)\.rb$}) { |m| "./spec/#{m[1]}_spec.rb" }
7
+ end
data/LIST_OF_ROW_TYPES.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [Email](#email)<br/>
5
5
  [Phone](#phone)<br/>
6
6
  [Number](#number)<br/>
7
+ [Currency](#currency)<br/>
7
8
  [Date](#date)
8
9
 
9
10
  **Other**<br/>
@@ -131,6 +132,21 @@ The `PhoneRow` is a string input row for a phone number and opens a `UIKeyboardT
131
132
  The `NumberRow` is a string input row for a number and opens a `UIKeyboardTypeDecimalPad` keyboard when editing.
132
133
 
133
134
 
135
+ ### <a name="currency"></a> Currency Row
136
+ ![Currency row](https://github.com/clayallsopp/formotion/wiki/row-types/currency_row.png)
137
+
138
+ ```ruby
139
+ {
140
+ title: "Amount",
141
+ key: :amount,
142
+ placeholder: "0.00",
143
+ type: :currency
144
+ }
145
+ ```
146
+
147
+ The `CurrentyRow` is a string input row with a number keyboard that restricts users to entering valid currency without tapping the decimal key on the keyboard.
148
+
149
+
134
150
  ### <a name="date"></a> Date row
135
151
  ![Date row](https://github.com/clayallsopp/formotion/wiki/row-types/date.png)
136
152
 
@@ -156,6 +172,19 @@ You can pass one of following formats as property:
156
172
 
157
173
  The `value` is a `NSDate.timeIntervalSince1970` as an integer.
158
174
 
175
+ Note that you can pass a `:picker_mode` argument to affect what kind of date picker is displayed:
176
+
177
+ ```ruby
178
+ :time # UIDatePickerModeTime
179
+ :date_time # UIDatePickerModeDateAndTime
180
+ :countdown # UIDatePickerModeCountDownTimer
181
+ ```
182
+
183
+ If none of these are specified, `UIDatePickerModeDate` is used.
184
+
185
+ Note: If you use `:date_time` or `:time` for the type, `:minute_interval` will be applied to the time picker if you specify a value for it.
186
+ the default is the Apple default of 1.
187
+
159
188
 
160
189
  ## Other
161
190
 
@@ -222,6 +251,13 @@ You can create a radio button style group by defining a section with `select_one
222
251
  }
223
252
  ```
224
253
 
254
+ This would result in a form's `render` as:
255
+
256
+ ```ruby
257
+ {
258
+ account_type: :basic
259
+ }
260
+ ```
225
261
 
226
262
  ### <a name="slider"></a> Slider row
227
263
  ![Slider row](https://github.com/clayallsopp/formotion/wiki/row-types/slider.png)
@@ -397,4 +433,4 @@ The `BackRow` is used in subforms to go back to the parent form.
397
433
  }
398
434
  ```
399
435
 
400
- The `EditRow` is used to switch between edit and view mode. It's mainly used for the `TemplateRow`.
436
+ The `EditRow` is used to switch between edit and view mode. It's mainly used for the `TemplateRow`.
data/NEW_ROW_TYPES.md CHANGED
@@ -53,8 +53,8 @@ module Formotion
53
53
  class MyNewRow < Base
54
54
  def build_cell(cell)
55
55
  blue_box = UIView.alloc.initWithFrame [[10, 10], [30, 30]]
56
- blux_box.backgroundColor = UIColor.blueColor
57
- cell.addSubview = blue_box
56
+ blue_box.backgroundColor = UIColor.blueColor
57
+ cell.addSubview blue_box
58
58
 
59
59
  # return nil because no UITextField added.
60
60
  nil
@@ -70,4 +70,4 @@ Your subclass can override these methods:
70
70
 
71
71
  `#on_select(tableView, tableViewDelegate)` - Called when the row is tapped by the user.
72
72
 
73
- `#cell_style` - The `UITableViewCellStyle` for the row type. By default is `UITableViewCellStyleSubtitle`
73
+ `#cell_style` - The `UITableViewCellStyle` for the row type. By default is `UITableViewCellStyleSubtitle`
data/README.md CHANGED
@@ -116,6 +116,25 @@ To add your own, check [the guide to adding new row types](https://github.com/cl
116
116
 
117
117
  `Formotion::Form`, `Formotion::Section`, and `Formotion::Row` all respond to a `::PROPERTIES` attribute. These are settable as an attribute (ie `section.title = 'title'`) or in the initialization hash (ie `{sections: [{title: 'title', ...}]}`). Check the comments in the 3 main files (`form.rb`, `section.rb`, and `row.rb` for details on what these do).
118
118
 
119
+ ### Setting Initial Values
120
+
121
+ Forms, particularly edit forms, have default initial values. You can supply these in the `:value` attribute for a given row. So, for example:
122
+
123
+ ```ruby
124
+ {
125
+ title: "Email",
126
+ key: :email,
127
+ placeholder: "me@mail.com",
128
+ type: :email,
129
+ auto_correction: :no,
130
+ auto_capitalization: :none,
131
+ value: 'zippity_dippity@doo.com'
132
+ }
133
+ ```
134
+
135
+ Setting values for non-string types can be tricky, so you need to watch what the particular field expects. In particular, date types require
136
+ the number of seconds from the beginning of the epoch as a number.
137
+
119
138
  ### Retrieve
120
139
 
121
140
  You have `form#submit`, `form#on_submit`, and `form#render` at your disposal. Here's an example:
@@ -175,7 +194,28 @@ Your form values and state are automatically persisted across application loads,
175
194
 
176
195
  To reset the form, `persist` it and call `reset`, which restores it to the original state.
177
196
 
178
- View the [Persistence Example](./formotion/tree/master/examples/Persistence) to see it in action.
197
+ View the [Persistence Example](./examples/Persistence) to see it in action.
198
+
199
+ ### Callbacks
200
+
201
+ `Row` objects support the following callbacks:
202
+
203
+ ```ruby
204
+ row.on_tap do |row|
205
+ p "I'm tapped!"
206
+ end
207
+
208
+ row.on_delete do |row|
209
+ p "I'm called before the delete animation"
210
+ end
211
+
212
+ row.on_begin do |row|
213
+ p "I'm called when my text field begins editing"
214
+ end
215
+
216
+ row.on_enter do |row|
217
+ p "I'm called when the user taps the return key while typing in my text field"
218
+ end
179
219
 
180
220
  ## Forking
181
221
 
data/Rakefile CHANGED
@@ -5,6 +5,7 @@ require "bundler/setup"
5
5
 
6
6
  $:.unshift("./lib/")
7
7
  require './lib/formotion'
8
+ require 'guard/motion'
8
9
 
9
10
  Motion::Project::App.setup do |app|
10
11
  # Use `rake config' to see complete project settings.
@@ -92,6 +92,13 @@ class AppDelegate
92
92
  key: :date_short,
93
93
  type: :date,
94
94
  format: :short
95
+ }, {
96
+ title: "Date (Time)",
97
+ subtitle: "w/picker_mode => :time",
98
+ key: :date_time_short,
99
+ type: :date,
100
+ picker_mode: :time,
101
+ format: :short
95
102
  }, {
96
103
  title: "Slider",
97
104
  key: :slider,
data/lib/formotion.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "formotion/version"
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "formotion/version"))
2
2
  require 'bubble-wrap/core'
3
3
  require 'bubble-wrap/camera'
4
4
 
@@ -17,6 +17,11 @@ BW.require File.expand_path('../formotion/**/*.rb', __FILE__) do
17
17
  ['date_row', 'email_row', 'number_row', 'phone_row'].each {|file|
18
18
  file("lib/formotion/row_type/#{file}.rb").depends_on 'lib/formotion/row_type/string_row.rb'
19
19
  }
20
+
21
+ ['currency_row'].each {|file|
22
+ file("lib/formotion/row_type/#{file}.rb").depends_on 'lib/formotion/row_type/number_row.rb'
23
+ }
24
+
20
25
  ['submit_row', 'back_row'].each {|file|
21
26
  file("lib/formotion/row_type/#{file}.rb").depends_on 'lib/formotion/row_type/button.rb'
22
27
  }
@@ -25,4 +30,4 @@ BW.require File.expand_path('../formotion/**/*.rb', __FILE__) do
25
30
  file("lib/formotion/#{file}").depends_on 'lib/formotion/base.rb'
26
31
  }
27
32
  file("lib/formotion/controller/form_controller.rb").depends_on 'lib/formotion/patch/ui_text_field.rb'
28
- end
33
+ end
@@ -22,7 +22,8 @@ module Formotion
22
22
 
23
23
  def self.persist(params = {})
24
24
  form = new(params)
25
- form.open && form.save
25
+ form.open
26
+ form.init_observer_for_save
26
27
  form
27
28
  end
28
29
 
@@ -65,8 +66,8 @@ module Formotion
65
66
  # EX
66
67
  # @form.create_section(:title => 'Section Title')
67
68
  def create_section(hash = {})
69
+ hash = hash.merge({:form => self})
68
70
  section = Formotion::Section.new(hash)
69
- section.form = self
70
71
  section.index = self.sections.count
71
72
  self.sections << section
72
73
  section
@@ -168,13 +169,12 @@ module Formotion
168
169
  else
169
170
  section.rows.each {|row|
170
171
  next if row.button?
171
- if row.template_parent
172
+ if row.templated?
172
173
  # If this row is part of a template
173
174
  # use the parent's key
174
- kv[row.template_parent.key] ||= []
175
- kv[row.template_parent.key] << row.value
175
+ kv[row.template_parent.key] = row.template_parent.value
176
176
  else
177
- kv[row.key] ||= row.value
177
+ kv[row.key] ||= row.value_for_save_hash
178
178
  end
179
179
  }
180
180
  end
@@ -200,7 +200,7 @@ module Formotion
200
200
  # see if one of the select one value is used
201
201
  unless (section.rows.map{ |r| r.key } & data.keys).empty?
202
202
  section.rows.each { |row|
203
- row.value = data.has_key?(row.key) ? true : nil
203
+ row.value = data.has_key?(row.key) ? true : nil
204
204
  }
205
205
  end
206
206
  else
@@ -221,25 +221,20 @@ module Formotion
221
221
  end
222
222
 
223
223
  alias_method :fill_out, :values=
224
-
224
+
225
225
  #########################
226
226
  # Persisting Forms
227
227
 
228
228
  # loads the given settings into the the form, and
229
229
  # places observers to save on changes
230
- def open
231
- @form_observer ||= lambda { |form, saved_form|
230
+
231
+ def init_observer_for_save
232
+ @form_save_observer ||= lambda { |form|
232
233
  form.sections.each_with_index do |section, s_index|
233
234
  section.rows.each_with_index do |row, index|
234
- temp_row = saved_form.sections[s_index].rows[index]
235
-
236
235
  if row.subform?
237
- @saved_subform = temp_row.subform.to_form
238
- @form_observer.call(row.subform.to_form, @saved_subform)
239
- @saved_subform = nil
240
- else
241
- row.value = temp_row.value
242
-
236
+ @form_save_observer.call(row.subform.to_form)
237
+ elsif !row.templated?
243
238
  observe(row, "value") do |old_value, new_value|
244
239
  self.save
245
240
  end
@@ -248,41 +243,48 @@ module Formotion
248
243
  end
249
244
  }
250
245
 
251
- saved_hash = load_state
252
- @form_observer.call(self, Formotion::Form.new(saved_hash))
253
- end
254
-
255
- # places hash of values into application persistance
256
- def save
257
- App::Persistence[persist_key] = to_hash.to_archived_data
258
- App::Persistence[original_persist_key] ||= self.to_hash.to_archived_data
246
+ @form_save_observer.call(self)
259
247
  end
260
248
 
261
- def reset
262
- App::Persistence[persist_key] = nil
263
-
264
- @form_resetter ||= lambda { |form, original_form|
249
+ def open
250
+ @form_observer ||= lambda { |form, saved_render|
265
251
  form.sections.each_with_index do |section, s_index|
266
252
  section.rows.each_with_index do |row, index|
267
- temp_row = original_form.sections[s_index].rows[index]
253
+ next if row.templated?
254
+ saved_row_value = saved_render[row.key]
268
255
 
269
256
  if row.subform?
270
- original_subform = temp_row.subform.to_form
271
- @form_resetter.call(row.subform.to_form, original_subform)
257
+ @form_observer.call(row.subform.to_form, saved_row_value)
258
+ elsif row.type == :template
259
+ row.value = saved_row_value
260
+ row.object.update_template_rows
272
261
  else
273
- row.value = temp_row.value
262
+ row.value = saved_row_value
274
263
  end
275
264
  end
276
265
  end
277
266
  }
267
+ rendered_data = load_state
268
+ if rendered_data
269
+ @form_observer.call(self, rendered_data)
270
+ else
271
+ save
272
+ end
273
+ end
278
274
 
279
- temp_form = Formotion::Form.new(App::Persistence[original_persist_key].unarchive)
280
- @form_resetter.call(self, temp_form)
275
+ # places hash of values into application persistance
276
+ def save
277
+ App::Persistence[persist_key] = render
278
+ App::Persistence[original_persist_key] ||= render
279
+ end
281
280
 
282
- self.save
281
+ def reset
282
+ App::Persistence[persist_key] = App::Persistence[original_persist_key]
283
+ open
283
284
  end
284
285
 
285
286
  private
287
+
286
288
  def persist_key
287
289
  "FORMOTION_#{self.persist_as}"
288
290
  end
@@ -292,13 +294,16 @@ module Formotion
292
294
  end
293
295
 
294
296
  def load_state
295
- begin
296
- state = App::Persistence[persist_key] && App::Persistence[persist_key].unarchive
297
- rescue
298
- self.save
297
+ state = App::Persistence[persist_key]
298
+ # support old archived format
299
+ if state.respond_to? :unarchive
300
+ begin
301
+ form = Formotion::Form.new(state.unarchive)
302
+ state = form.render
303
+ p "Old archived data found: #{state}"
304
+ end
299
305
  end
300
-
301
- state ||= self.to_hash
306
+ state
302
307
  end
303
308
 
304
309
  def each_row(&block)
@@ -18,7 +18,7 @@ module Formotion
18
18
  def controller=(controller)
19
19
  @controller = controller
20
20
  @controller.title = self.title
21
- self.table = controller.respond_to?(:table_view) ? controller.table_view : controller.tableView
21
+ self.table = @controller.tableView
22
22
  end
23
23
 
24
24
  def table=(table_view)
@@ -104,4 +104,4 @@ module Formotion
104
104
  row.object.indentWhileEditing?
105
105
  end
106
106
  end
107
- end
107
+ end
@@ -15,12 +15,6 @@
15
15
  # end
16
16
 
17
17
  class UITextField
18
- attr_accessor :menu_options_enabled
19
-
20
- def canPerformAction(action, withSender:sender)
21
- self.menu_options_enabled
22
- end
23
-
24
18
  # block takes argument textField; should return true/false
25
19
  def should_begin?(&block)
26
20
  add_delegate_method do
@@ -155,4 +149,4 @@ class UITextField_Delegate
155
149
  end
156
150
  true
157
151
  end
158
- end
152
+ end
@@ -15,12 +15,6 @@
15
15
  # end
16
16
 
17
17
  class UITextView
18
- attr_accessor :menu_options_enabled
19
-
20
- def canPerformAction(action, withSender:sender)
21
- self.menu_options_enabled.nil? ? true : self.menu_options_enabled
22
- end
23
-
24
18
  # block takes argument textView; should return true/false
25
19
  def should_begin?(&block)
26
20
  add_delegate_method do
@@ -82,7 +82,10 @@ module Formotion
82
82
  # When a row is deleted, actually remove the row from UI
83
83
  # instead of just nil'ing the value.
84
84
  # DEFAULT is false EXCEPT for template-generated rows
85
- :remove_on_delete
85
+ :remove_on_delete,
86
+ # In a date/time or time picker, the minute interval can
87
+ # be set. That allows picking by every 15 minutes, etc.
88
+ :minute_interval
86
89
  ]
87
90
  PROPERTIES.each {|prop|
88
91
  attr_accessor prop
@@ -118,12 +121,15 @@ module Formotion
118
121
  # callback for what happens when the user
119
122
  # taps a ButtonRow
120
123
  attr_accessor :on_tap_callback
124
+ # callback for when a row is tapped
125
+ attr_accessor :on_delete_callback
121
126
 
122
127
  # RowType object
123
128
  attr_accessor :object
124
129
 
125
130
  # Owning template row, if applicable
126
131
  attr_accessor :template_parent
132
+ attr_accessor :template_children
127
133
 
128
134
  def initialize(params = {})
129
135
  super
@@ -131,20 +137,27 @@ module Formotion
131
137
  BOOLEAN_PROPERTIES.each { |prop|
132
138
  Formotion::Conditions.assert_nil_or_boolean(self.send(prop))
133
139
  }
140
+ @template_children = []
134
141
  end
135
142
 
136
143
  # called after section and index have been assigned
137
144
  def after_create
138
- if self.type == :template and (self.value && self.value.any?)
139
- self.value.each do |value|
140
- new_row = self.object.build_new_row({:value => value})
141
- end
145
+ if self.type == :template
146
+ self.object.update_template_rows
142
147
  end
143
148
  end
144
149
 
145
150
  #########################
146
151
  # pseudo-properties
147
152
 
153
+ def value_for_save_hash
154
+ if self.object.respond_to? :value_for_save_hash
155
+ self.object.value_for_save_hash
156
+ else
157
+ self.value
158
+ end
159
+ end
160
+
148
161
  def index_path
149
162
  NSIndexPath.indexPathForRow(self.index, inSection:self.section.index)
150
163
  end
@@ -184,6 +197,10 @@ module Formotion
184
197
  self.type.to_s == "subform"
185
198
  end
186
199
 
200
+ def templated?
201
+ !!self.template_parent
202
+ end
203
+
187
204
  #########################
188
205
  # getter overrides
189
206
  def items
@@ -273,6 +290,10 @@ module Formotion
273
290
  self.on_tap_callback = block
274
291
  end
275
292
 
293
+ def on_delete(&block)
294
+ self.on_delete_callback = block
295
+ end
296
+
276
297
  #########################
277
298
  # Methods for making cells
278
299
  # Called in UITableViewDataSource methods
@@ -57,15 +57,26 @@ module Formotion
57
57
  # method gets triggered when tableView(tableView, didSelectRowAtIndexPath:indexPath)
58
58
  # in UITableViewDelegate is executed
59
59
  def on_select(tableView, tableViewDelegate)
60
- # implement in row class
60
+ # row class should call super and proceed if false is return (not handled here)
61
+ if row.on_tap_callback
62
+ # Not all row types will want to define on_tap, but call it if so
63
+ row.on_tap_callback.call(self.row)
64
+ true
65
+ else
66
+ false
67
+ end
61
68
  end
62
69
 
63
70
  # called when the delete editing style was triggered tableView:commitEditingStyle:forRowAtIndexPath:
64
71
  def on_delete(tableView, tableViewDelegate)
72
+ if row.on_delete_callback
73
+ row.on_delete_callback.call(self.row)
74
+ end
65
75
  if row.remove_on_delete?
66
76
  row.section.rows.delete_at(row.index)
67
77
  row.section.refresh_row_indexes
68
78
  delete_row
79
+ after_delete
69
80
  else
70
81
  row.value = nil
71
82
  self.tableView.reloadData
@@ -78,6 +89,9 @@ module Formotion
78
89
  tableView.endUpdates
79
90
  end
80
91
 
92
+ def after_delete
93
+ end
94
+
81
95
  def break_with_semaphore(&block)
82
96
  return if @semaphore
83
97
  with_semaphore(&block)
@@ -90,4 +104,4 @@ module Formotion
90
104
  end
91
105
  end
92
106
  end
93
- end
107
+ end
@@ -23,13 +23,6 @@ module Formotion
23
23
  end
24
24
  nil
25
25
  end
26
-
27
- def on_select(tableView, tableViewDelegate)
28
- # Override in subclasses
29
- if self.row.on_tap_callback
30
- self.row.on_tap_callback.call(self.row)
31
- end
32
- end
33
26
  end
34
27
  end
35
- end
28
+ end
@@ -0,0 +1,43 @@
1
+ module Formotion
2
+ module RowType
3
+ class CurrencyRow < NumberRow
4
+ def on_change(text_field)
5
+ break_with_semaphore do
6
+ edited_text = text_field.text
7
+ entered_digits = edited_text.gsub %r{\D+}, ''
8
+ decimal_num = 0.0
9
+
10
+ if !entered_digits.empty?
11
+ decimal_num = entered_digits.to_i * (10 ** currency_scale.to_i)
12
+ decimal_num = decimal_num.to_f
13
+ end
14
+
15
+ row.value = decimal_num
16
+ text_field.text = row_value
17
+ end
18
+ end
19
+
20
+ def row_value
21
+ number_formatter.stringFromNumber super.to_f
22
+ end
23
+
24
+ def value_for_save_hash
25
+ number_formatter.numberFromString(row_value)
26
+ end
27
+
28
+ private
29
+ def number_formatter
30
+ @number_formatter ||= begin
31
+ formatter = NSNumberFormatter.alloc.init
32
+ formatter.numberStyle = NSNumberFormatterCurrencyStyle
33
+ formatter.locale = NSLocale.currentLocale
34
+ formatter
35
+ end
36
+ end
37
+
38
+ def currency_scale
39
+ @currency_scale ||= number_formatter.maximumFractionDigits * -1
40
+ end
41
+ end
42
+ end
43
+ end
@@ -12,7 +12,7 @@ module Formotion
12
12
  def date_value
13
13
  value = self.row.value
14
14
  if value.is_a? Numeric
15
- NSDate.dateWithTimeIntervalSince1970(value.to_i)
15
+ Time.at value
16
16
  else
17
17
  nil
18
18
  end
@@ -43,13 +43,14 @@ module Formotion
43
43
  picker = UIDatePicker.alloc.initWithFrame(CGRectZero)
44
44
  picker.datePickerMode = self.picker_mode
45
45
  picker.hidden = false
46
- picker.date = self.date_value || NSDate.date
46
+ picker.date = self.date_value || Time.now
47
+ picker.minuteInterval = self.row.minute_interval if self.row.minute_interval
47
48
 
48
49
  picker.when(UIControlEventValueChanged) do
49
50
  if self.row.picker_mode == :countdown
50
51
  self.row.value = @picker.countDownDuration
51
52
  else
52
- self.row.value = @picker.date.timeIntervalSince1970.to_i
53
+ self.row.value = Time.at(@picker.date).to_i
53
54
  end
54
55
  update
55
56
  end
@@ -1,12 +1,18 @@
1
1
  module Formotion
2
2
  module RowType
3
3
  class ImageRow < Base
4
+ TAKE = BW.localized_string("Take", nil)
5
+ DELETE = BW.localized_string("Delete", nil)
6
+ CHOOSE = BW.localized_string("Choose", nil)
7
+ CANCEL = BW.localized_string("Cancel", nil)
8
+
4
9
  include BW::KVO
5
10
 
6
11
  IMAGE_VIEW_TAG=1100
7
12
 
8
13
  def build_cell(cell)
9
- add_plus_accessory(cell)
14
+ # only show the "plus" when editable
15
+ add_plus_accessory(cell) if row.editable?
10
16
 
11
17
  observe(self.row, "value") do |old_value, new_value|
12
18
  @image_view.image = new_value
@@ -15,7 +21,8 @@ module Formotion
15
21
  cell.accessoryView = cell.editingAccessoryView = nil
16
22
  else
17
23
  self.row.row_height = 44
18
- add_plus_accessory(cell)
24
+ # only show the "plus" when editable
25
+ add_plus_accessory(cell) if row.editable?
19
26
  end
20
27
  row.form.reload_data
21
28
  end
@@ -51,10 +58,10 @@ module Formotion
51
58
  @action_sheet = UIActionSheet.alloc.init
52
59
  @action_sheet.delegate = self
53
60
 
54
- @action_sheet.destructiveButtonIndex = (@action_sheet.addButtonWithTitle "Delete") if row.value
55
- @action_sheet.addButtonWithTitle "Take" if BW::Device.camera.front? or BW::Device.camera.rear?
56
- @action_sheet.addButtonWithTitle "Choose"
57
- @action_sheet.cancelButtonIndex = (@action_sheet.addButtonWithTitle "Cancel")
61
+ @action_sheet.destructiveButtonIndex = (@action_sheet.addButtonWithTitle DELETE) if row.value
62
+ @action_sheet.addButtonWithTitle TAKE if BW::Device.camera.front? or BW::Device.camera.rear?
63
+ @action_sheet.addButtonWithTitle CHOOSE
64
+ @action_sheet.cancelButtonIndex = (@action_sheet.addButtonWithTitle CANCEL)
58
65
 
59
66
  @action_sheet.showInView @image_view
60
67
  end
@@ -68,11 +75,11 @@ module Formotion
68
75
  end
69
76
 
70
77
  case actionSheet.buttonTitleAtIndex(index)
71
- when "Take"
78
+ when TAKE
72
79
  source = :camera
73
- when "Choose"
80
+ when CHOOSE
74
81
  source = :photo_library
75
- when "Cancel"
82
+ when CANCEL
76
83
  else
77
84
  p "Unrecognized button title #{actionSheet.buttonTitleAtIndex(index)}"
78
85
  end
@@ -99,4 +106,4 @@ module Formotion
99
106
  end
100
107
  end
101
108
  end
102
- end
109
+ end
@@ -8,4 +8,4 @@ module Formotion
8
8
 
9
9
  end
10
10
  end
11
- end
11
+ end
@@ -128,16 +128,21 @@ module Formotion
128
128
  end
129
129
 
130
130
  def on_select(tableView, tableViewDelegate)
131
+ super or _on_select(tableView, tableViewDelegate)
132
+ end
133
+
134
+ def _on_select(tableView, tableViewDelegate)
131
135
  if !row.editable?
132
136
  return
137
+ else
138
+ row.text_field.becomeFirstResponder
133
139
  end
134
- row.text_field.becomeFirstResponder
135
140
  end
136
141
 
137
142
  # Used when row.value changes
138
143
  def update_text_field(new_value)
139
- self.row.text_field.text = new_value
144
+ self.row.text_field.text = row_value
140
145
  end
141
146
  end
142
147
  end
143
- end
148
+ end
@@ -14,6 +14,7 @@
14
14
  module Formotion
15
15
  module RowType
16
16
  class TemplateRow < Base
17
+ include BubbleWrap::KVO
17
18
 
18
19
  def cellEditingStyle
19
20
  UITableViewCellEditingStyleInsert
@@ -33,7 +34,6 @@ module Formotion
33
34
  button
34
35
  end
35
36
  cell.accessoryView = @add_button
36
-
37
37
  nil
38
38
  end
39
39
 
@@ -42,7 +42,6 @@ module Formotion
42
42
  end
43
43
 
44
44
  def on_insert(tableView, tableViewDelegate)
45
- @template_index = row.section.rows.count
46
45
  new_row = build_new_row
47
46
  move_row_in_list(new_row)
48
47
  insert_row(new_row)
@@ -51,27 +50,76 @@ module Formotion
51
50
  def build_new_row(options = {})
52
51
  # build row
53
52
  new_row = row.section.create_row(row.template.merge(options))
53
+ new_row.object.instance_eval do
54
+ def after_delete
55
+ template_value = row.template_parent.value
56
+ template_value.delete_at(row.index)
57
+ row.template_parent.value = template_value
58
+ end
59
+ end
54
60
  new_row.remove_on_delete = true
55
- new_row.template_parent = self.row
61
+ new_row.template_parent = row
62
+ row.template_children ||= []
63
+ row.template_children << new_row
64
+ observe(new_row, "value") do |old_value, new_value|
65
+ template_value = row.value.dup
66
+ template_value[new_row.index] = new_row.value
67
+ row.value = template_value
68
+ end
56
69
  new_row
57
70
  end
58
71
 
59
72
  def move_row_in_list(new_row)
60
73
  # move to top
61
74
  row.section.rows.pop
62
- row.section.rows.insert(@template_index - 1, new_row)
75
+ row.section.rows.insert(row.template_children.count - 1, new_row)
63
76
 
64
77
  # reset indexes
65
78
  row.section.refresh_row_indexes
66
79
  end
67
80
 
68
- def insert_row(new_row)
69
- index_path = NSIndexPath.indexPathForRow(new_row.index, inSection:row.section.index)
81
+ def insert_row(template_row, with_animation = true)
82
+ animation = with_animation ? UITableViewRowAnimationBottom : UITableViewRowAnimationNone
83
+ index_path = NSIndexPath.indexPathForRow(template_row.index, inSection:row.section.index)
84
+ tableView.beginUpdates
85
+ tableView.insertRowsAtIndexPaths [index_path], withRowAnimation:animation
86
+ tableView.endUpdates
87
+ end
88
+
89
+ def delete_row(template_row, with_animation = true)
90
+ animation = with_animation ? UITableViewRowAnimationTop : UITableViewRowAnimationNone
91
+ index_path = NSIndexPath.indexPathForRow(template_row.index, inSection:row.section.index)
70
92
  tableView.beginUpdates
71
- tableView.insertRowsAtIndexPaths [index_path], withRowAnimation:UITableViewRowAnimationBottom
93
+ tableView.deleteRowsAtIndexPaths [index_path], withRowAnimation:animation
72
94
  tableView.endUpdates
73
95
  end
74
96
 
97
+ def update_template_rows
98
+ row.value ||= []
99
+ if row.value.count > row.template_children.count
100
+ row.value[row.template_children.count..-1].each do |value|
101
+ new_row = build_new_row({:value => value})
102
+ move_row_in_list(new_row)
103
+ insert_row(new_row) if tableView
104
+ end
105
+ elsif row.value.count < row.template_children.count
106
+ row.template_children[row.value.count..-1].each do |row_to_delete|
107
+ row.section.rows.delete(row_to_delete)
108
+ row.template_children.delete(row_to_delete)
109
+ row.section.refresh_row_indexes
110
+ delete_row(row_to_delete) if tableView
111
+ end
112
+ end
113
+
114
+ row.value.dup.each_with_index do |new_value, index|
115
+ template_child = row.template_children[index]
116
+ old_value = template_child.value
117
+ if old_value != new_value
118
+ template_child.value = new_value
119
+ end
120
+ end
121
+ end
122
+
75
123
  end
76
124
  end
77
125
  end
@@ -29,7 +29,7 @@ module Formotion
29
29
 
30
30
  def initialize(params = {})
31
31
  super
32
-
32
+ self.form = params[:form]
33
33
  Formotion::Conditions.assert_nil_or_boolean(self.select_one)
34
34
 
35
35
  rows = params[:rows] || params["rows"]
@@ -112,7 +112,10 @@ module Formotion
112
112
  # Retreiving data
113
113
  def to_hash
114
114
  h = super
115
- h[:rows] = self.rows.collect {|row| row.to_hash}
115
+ h[:rows] = []
116
+ self.rows.each do |row|
117
+ h[:rows] << row.to_hash if row.template_parent.nil?
118
+ end
116
119
  h
117
120
  end
118
121
  end
@@ -1,3 +1,3 @@
1
1
  module Formotion
2
- VERSION = "1.2"
2
+ VERSION = "1.3"
3
3
  end
@@ -19,8 +19,8 @@ describe "Form Persisting" do
19
19
  r = f.sections[0].rows[0]
20
20
  r.value = "new value"
21
21
 
22
- saved = Formotion::Form.new(f.send(:load_state))
23
- saved.sections[0].rows[0].value.should == r.value
22
+ saved = f.send(:load_state)
23
+ saved["first"] == r.value
24
24
 
25
25
  f.reset
26
26
  r.value.should == "initial value"
@@ -57,10 +57,45 @@ describe "Form Persisting" do
57
57
  r = f.sections[0].rows[0].subform.to_form.sections[0].rows[0]
58
58
  r.value = "new value"
59
59
 
60
- saved = Formotion::Form.new(f.send(:load_state))
61
- saved.sections[0].rows[0].subform.to_form.sections[0].rows[0].value.should == r.value
60
+ saved = f.send(:load_state)
61
+ saved[:subform]["second"].should == r.value
62
62
 
63
63
  f.reset
64
64
  r.value.should == "initial value"
65
65
  end
66
+
67
+ it "works with templates" do
68
+ key = "test_#{rand(255)}"
69
+ App::Persistence["FORMOTION_#{key}"] = nil
70
+ App::Persistence["FORMOTION_#{key}_ORIGINAL"] = nil
71
+ hash = {
72
+ persist_as: key,
73
+ sections: [
74
+ rows: [{
75
+ title: "Add nickname",
76
+ key: :nicknames,
77
+ type: :template,
78
+ value: ['Nici', 'Sam'],
79
+ template: {
80
+ title: 'Nickname',
81
+ type: :string,
82
+ placeholder: 'Enter here',
83
+ indented: true,
84
+ deletable: true
85
+ }
86
+ }]
87
+ ]
88
+ }
89
+ f = Formotion::Form.persist(hash)
90
+ f.render.should == { :nicknames => ['Nici', 'Sam'] }
91
+
92
+ r = f.sections[0].rows[0]
93
+ r.value = "Sandra"
94
+
95
+ saved = f.send(:load_state)
96
+ saved[:nicknames].should == ["Sandra", "Sam"]
97
+
98
+ f.reset
99
+ r.value.should == "Nici"
100
+ end
66
101
  end
@@ -0,0 +1,38 @@
1
+ module Formotion
2
+ module RowType
3
+ class DummyRow < Base
4
+ end
5
+ end
6
+ end
7
+
8
+ describe "Base Row Type" do
9
+
10
+ describe '#on_select' do
11
+ tests_row :dummy
12
+
13
+ it "should return false if callback is not defined" do
14
+ @row.object.on_select(nil, nil).should == false
15
+ end
16
+
17
+ describe 'when on_tap_callback is set' do
18
+ tests_row :dummy do |row|
19
+ row.on_tap { |row| @called = true }
20
+ end
21
+
22
+ before do
23
+ @called = false
24
+ end
25
+
26
+ it "should return true" do
27
+ @row.object.on_select(nil, nil).should == true
28
+ end
29
+
30
+ it "should call the callback" do
31
+ @row.object.on_select(nil, nil)
32
+ @called.should == true
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ describe "Number Row" do
2
+ tests_row :currency
3
+
4
+ def set_formatter_locale(locale)
5
+ formatter = NSNumberFormatter.alloc.init
6
+ formatter.numberStyle = NSNumberFormatterCurrencyStyle
7
+ formatter.locale = NSLocale.alloc.initWithLocaleIdentifier(locale)
8
+ @row.object.instance_variable_set("@number_formatter", formatter)
9
+ end
10
+
11
+ it "should initialize with correct settings" do
12
+ cell = @row.make_cell
13
+ @row.object.class.should == Formotion::RowType::CurrencyRow
14
+ set_formatter_locale("en_US")
15
+ @row.value.should == 0.0
16
+ @row.object.row_value.should == "$0.00"
17
+ @row.value_for_save_hash.should == 0.0
18
+ end
19
+
20
+ it "should work when row#value is changed" do
21
+ cell = @row.make_cell
22
+ set_formatter_locale("en_US")
23
+ @row.value = 4.35
24
+ @row.text_field.text.should == "$4.35"
25
+ end
26
+
27
+ it "should work when text_field#text is changed" do
28
+ cell = @row.make_cell
29
+ set_formatter_locale("en_US")
30
+ @row.text_field.setText("$100.45")
31
+ @row.text_field.delegate.on_change(@row.text_field)
32
+ @row.value.should == 100.45
33
+ end
34
+
35
+ it "should work with different locale" do
36
+ cell = @row.make_cell
37
+ set_formatter_locale("it_IT")
38
+ @row.value = 3004.35
39
+ @row.text_field.text.should == "€ 3.004,35"
40
+ end
41
+
42
+ it "should work with different locale and setting text_field#text" do
43
+ cell = @row.make_cell
44
+ set_formatter_locale("it_IT")
45
+ @row.text_field.setText("€ 3.100,35")
46
+ @row.text_field.delegate.on_change(@row.text_field)
47
+ @row.value.should == 3100.35
48
+ end
49
+ end
@@ -1,4 +1,4 @@
1
- MILLENIUM = 946684672
1
+ MILLENIUM = 946684772 # Time.at(MILLENIUM) => 1999-12-31 18:59:32 -0500
2
2
  TIME_ZONE = NSTimeZone.timeZoneWithName "Europe/Paris"
3
3
 
4
4
  describe "Date Row" do
@@ -34,7 +34,7 @@ describe "Date Row" do
34
34
  it "should update value when date is picked" do
35
35
  cell = @row.make_cell
36
36
 
37
- @row.object.picker.date = NSDate.dateWithTimeIntervalSince1970(MILLENIUM)
37
+ @row.object.picker.date = Time.at MILLENIUM
38
38
  @row.object.picker.trigger UIControlEventValueChanged
39
39
 
40
40
  @row.value.should == MILLENIUM
@@ -63,16 +63,16 @@ describe "Date Row" do
63
63
  # Modes
64
64
  {
65
65
  :date => '1/1/00',
66
- :time => '12:57 AM',
67
- :date_time => '1/1/00, 12:57 AM',
68
- :countdown => '00:57'
66
+ :time => '12:59 AM',
67
+ :date_time => '1/1/00, 12:59 AM',
68
+ :countdown => '00:59'
69
69
  }.each do |mode, expected_output|
70
70
 
71
71
  it "should display chosen mode #{mode} date/time format #{expected_output}" do
72
72
  @row.format = :short
73
73
  @row.picker_mode = mode
74
74
  cell = @row.make_cell
75
- @row.object.picker.date = NSDate.dateWithTimeIntervalSince1970(MILLENIUM)
75
+ @row.object.picker.date = Time.at MILLENIUM
76
76
  @row.object.picker.trigger UIControlEventValueChanged
77
77
 
78
78
  @row.text_field.text.should == expected_output
@@ -53,4 +53,33 @@ describe "String Row Type" do
53
53
  cell = @row.make_cell
54
54
  @row.text_field.keyboardType.should == UIKeyboardTypeDefault
55
55
  end
56
- end
56
+
57
+ describe "on_select" do
58
+
59
+ it "should call _on_select" do
60
+ @row.object.instance_variable_set("@called_on_select", false)
61
+ def (@row.object)._on_select(a, b); @called_on_select = true end
62
+ @row.object.on_select(nil, nil)
63
+ @row.object.instance_variable_get("@called_on_select").should == true
64
+ end
65
+
66
+ describe "when on_tap callback is set" do
67
+ tests_row :string do |row|
68
+ @on_tap_called = false
69
+ row.on_tap { |row| @on_tap_called = true }
70
+ end
71
+
72
+ it "should return true" do
73
+ @row.object.on_select(nil, nil).should == true
74
+ end
75
+
76
+ it "should call the callback" do
77
+ @row.object.on_select(nil, nil)
78
+ @on_tap_called.should == true
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: formotion
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.2'
4
+ version: '1.3'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-03 00:00:00.000000000 Z
12
+ date: 2013-03-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bubble-wrap
@@ -54,6 +54,7 @@ files:
54
54
  - CHANGELOG.md
55
55
  - Formotion.gemspec
56
56
  - Gemfile
57
+ - Guardfile
57
58
  - LICENSE
58
59
  - LIST_OF_ROW_TYPES.md
59
60
  - NEW_ROW_TYPES.md
@@ -125,6 +126,7 @@ files:
125
126
  - lib/formotion/row_type/base.rb
126
127
  - lib/formotion/row_type/button.rb
127
128
  - lib/formotion/row_type/check_row.rb
129
+ - lib/formotion/row_type/currency_row.rb
128
130
  - lib/formotion/row_type/date_row.rb
129
131
  - lib/formotion/row_type/edit_row.rb
130
132
  - lib/formotion/row_type/email_row.rb
@@ -166,7 +168,9 @@ files:
166
168
  - spec/helpers/row_test_helpers.rb
167
169
  - spec/row_spec.rb
168
170
  - spec/row_type/back_spec.rb
171
+ - spec/row_type/base_spec.rb
169
172
  - spec/row_type/check_spec.rb
173
+ - spec/row_type/currency_spec.rb
170
174
  - spec/row_type/date_spec.rb
171
175
  - spec/row_type/email_spec.rb
172
176
  - spec/row_type/image_spec.rb
@@ -198,7 +202,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
198
202
  version: '0'
199
203
  segments:
200
204
  - 0
201
- hash: 3006402350062917066
205
+ hash: -3419813869520045411
202
206
  required_rubygems_version: !ruby/object:Gem::Requirement
203
207
  none: false
204
208
  requirements:
@@ -207,7 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
211
  version: '0'
208
212
  segments:
209
213
  - 0
210
- hash: 3006402350062917066
214
+ hash: -3419813869520045411
211
215
  requirements: []
212
216
  rubyforge_project:
213
217
  rubygems_version: 1.8.23
@@ -236,7 +240,9 @@ test_files:
236
240
  - spec/helpers/row_test_helpers.rb
237
241
  - spec/row_spec.rb
238
242
  - spec/row_type/back_spec.rb
243
+ - spec/row_type/base_spec.rb
239
244
  - spec/row_type/check_spec.rb
245
+ - spec/row_type/currency_spec.rb
240
246
  - spec/row_type/date_spec.rb
241
247
  - spec/row_type/email_spec.rb
242
248
  - spec/row_type/image_spec.rb