formotion 1.2 → 1.3

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