formotion 1.0 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,15 @@
1
- ## 1.1 - ???
1
+ ## 1.1 - September 20, 2012
2
+
3
+ ### Features
4
+
5
+ -:display_key for subform row; when given, the value for that key in the subform's `#render` will be displayed alongside the chevron.
6
+
7
+ -`Formotion::Form.persist` and `:persist_as` to automatically synchronize your form to the disk.
2
8
 
3
9
  ### Bug Fixes
4
10
 
11
+ - Fix problems with inheriting Formable models.
12
+
5
13
  ## 1.0 - August 11, 2012
6
14
 
7
15
  ### Features
@@ -2,6 +2,7 @@
2
2
  [String](#string)<br/>
3
3
  [Text](#text)<br/>
4
4
  [Email](#email)<br/>
5
+ [Phone](#phone)<br/>
5
6
  [Number](#number)<br/>
6
7
  [Date](#date)
7
8
 
@@ -11,14 +12,49 @@
11
12
  [Check](#check)<br/>
12
13
  [Slider](#slider)<br/>
13
14
  [Image](#image)<br/>
14
- [Options](#options)
15
+ [Option](#option)<br/>
16
+ [Picker](#picker)<br/>
17
+ [Subform](#subform)<br/>
18
+ [Template](#template)
15
19
 
16
20
  **Buttons**<br/>
17
- [Submit](#submit)
21
+ [Submit](#submit)<br/>
22
+ [Back](#back)<br/>
23
+ [Edit](#edit)<br/>
24
+
25
+ ## General
26
+
27
+ All row types accept following properties:
28
+ ```ruby
29
+ {
30
+ key: :user_name, # Unique identifier for the row
31
+ value: 'Some Value', # The initial value passed to the row
32
+ title: 'Title', # Title of the row
33
+ subtitle: 'Subtitle', # Subtitle of the row
34
+ type: :string, # The type of row (string, phone, switch, etc)
35
+ row_height: 100 # Height of the row
36
+ }
37
+ ```
38
+
39
+ The `type` property will define what kind of row type will get built. It looks for any Class within the `Formotion::RowType` namespace that has the constantized name (e.g. Formotion::RowType::StringRow).
40
+
18
41
 
19
42
  ## Character based
20
43
 
44
+ All character based row types accept following properties:
45
+ ```ruby
46
+ {
47
+ placeholder: 'James Bond', # Placeholder before a value exists
48
+ auto_correction: :no, # Disable auto correction
49
+ auto_capitalization: :none, # Disable auto capitalization
50
+ secure: true, # Enable secure input (Password)
51
+ clear_button: :while_editing, # Enable clear button
52
+ return_key: :google, # Define return key
53
+ }
54
+ ```
55
+
21
56
  ### <a name="string"></a> String row
57
+
22
58
  ![String row](https://github.com/clayallsopp/formotion/wiki/row-types/string.png)
23
59
  ```ruby
24
60
  {
@@ -31,6 +67,9 @@
31
67
  }
32
68
  ```
33
69
 
70
+ The `StringRow` is a simple text input row and opens a `UIKeyboardTypeDefault` keyboard when editing.
71
+
72
+
34
73
  ### <a name="text"></a> Text row
35
74
  ![Text Row](https://github.com/clayallsopp/formotion/wiki/row-types/text.png)
36
75
 
@@ -44,6 +83,10 @@
44
83
  }
45
84
  ```
46
85
 
86
+ The `TextRow` is a multiline text input row and opens the default keyboard when editing.
87
+ To define the height of the row set the property `row_height`.
88
+
89
+
47
90
  ### <a name="email"></a> Email row
48
91
  ![Email row](https://github.com/clayallsopp/formotion/wiki/row-types/email.png)
49
92
  ```ruby
@@ -55,6 +98,23 @@
55
98
  }
56
99
  ```
57
100
 
101
+ The `EmailRow` is a string input row for an email address and opens a `UIKeyboardTypeEmailAddress` keyboard when editing.
102
+
103
+
104
+ ### <a name="phone"></a> Phone row
105
+ ![Email row](https://github.com/clayallsopp/formotion/wiki/row-types/phone.png)
106
+ ```ruby
107
+ {
108
+ title: "Phone",
109
+ key: :phone,
110
+ type: :phone,
111
+ placeholder: '+01 877 412 7753'
112
+ }
113
+ ```
114
+
115
+ The `PhoneRow` is a string input row for a phone number and opens a `UIKeyboardTypePhonePad` keyboard when editing.
116
+
117
+
58
118
  ### <a name="number"></a> Number row
59
119
  ![Number row](https://github.com/clayallsopp/formotion/wiki/row-types/number.png)
60
120
 
@@ -67,6 +127,9 @@
67
127
  }
68
128
  ```
69
129
 
130
+ The `NumberRow` is a string input row for a number and opens a `UIKeyboardTypeDecimalPad` keyboard when editing.
131
+
132
+
70
133
  ### <a name="date"></a> Date row
71
134
  ![Date row](https://github.com/clayallsopp/formotion/wiki/row-types/date.png)
72
135
 
@@ -80,6 +143,19 @@
80
143
  }
81
144
  ```
82
145
 
146
+ The `DateRow` is a string input row for a date and opens a `UIDatePicker` when editing.
147
+ You can pass one of following formats as property:
148
+ ```ruby
149
+ :no # NSDateFormatterNoStyle - Specifies no style.
150
+ :short # NSDateFormatterShortStyle - Specifies a short style, typically numeric only, such as “11/23/37” or “3:30pm”.
151
+ :medium # NSDateFormatterMediumStyle - Specifies a medium style, typically with abbreviated text, such as “Nov 23, 1937”.
152
+ :long # NSDateFormatterLongStyle - Specifies a long style, typically with full text, such as “November 23, 1937” or “3:30:32pm”.
153
+ :full # NSDateFormatterFullStyle -Specifies a full style with complete details, such as “Tuesday, April 12, 1952 AD” or “3:30:42pm PST”.
154
+ ```
155
+
156
+ The `value` is a `NSDate.timeIntervalSince1970` as an integer.
157
+
158
+
83
159
  ## Other
84
160
 
85
161
  ### <a name="static"></a> Static row
@@ -91,6 +167,9 @@
91
167
  }
92
168
  ```
93
169
 
170
+ The `StaticRow` has no input funcitonality and therefore needs no `key` property. It is used for static text display like in Settings > General > About on your iOS device.
171
+
172
+
94
173
  ### <a name="switch"></a> Switch row
95
174
  ![Switch row](https://github.com/clayallsopp/formotion/wiki/row-types/switch.png)
96
175
 
@@ -102,6 +181,9 @@
102
181
  }
103
182
  ```
104
183
 
184
+ The `SwitchRow` is a on/off switch that has the `value` true or false.
185
+
186
+
105
187
  ### <a name="check"></a> Check row
106
188
  ![Check row](https://github.com/clayallsopp/formotion/wiki/row-types/check.png)
107
189
 
@@ -114,6 +196,32 @@
114
196
  }
115
197
  ```
116
198
 
199
+ The `CheckRow` is a row you can check and uncheck by tapping on it. It has either the `value` true or false.
200
+ You can create a radio button style group by defining a section with `select_one: true` like this:
201
+
202
+ ```ruby
203
+ {
204
+ title: "Account Type",
205
+ key: :account_type,
206
+ select_one: true,
207
+ rows: [{
208
+ title: "Free",
209
+ key: :free,
210
+ type: :check,
211
+ }, {
212
+ title: "Basic",
213
+ value: true,
214
+ key: :basic,
215
+ type: :check,
216
+ }, {
217
+ title: "Pro",
218
+ key: :pro,
219
+ type: :check,
220
+ }]
221
+ }
222
+ ```
223
+
224
+
117
225
  ### <a name="slider"></a> Slider row
118
226
  ![Slider row](https://github.com/clayallsopp/formotion/wiki/row-types/slider.png)
119
227
 
@@ -127,6 +235,8 @@
127
235
  }
128
236
  ```
129
237
 
238
+ The `SliderRow` takes a ruby range as `range` property that defines the min and max value of the slider.
239
+
130
240
  ### <a name="image"></a> Image row
131
241
  ![Image row](https://github.com/clayallsopp/formotion/wiki/row-types/image.png)
132
242
 
@@ -143,10 +253,91 @@
143
253
 
144
254
  ```ruby
145
255
  {
146
- title: "options",
147
- key: :options,
256
+ title: "Account Type",
257
+ key: :account_type,
148
258
  type: :options,
149
- items: ['One', 'Two']
259
+ items: ["Free", "Basic", "Pro"]
260
+ }
261
+ ```
262
+
263
+
264
+ ### <a name="picker"></a> Picker row
265
+ ![Picker row](https://github.com/clayallsopp/formotion/wiki/row-types/picker.png)
266
+
267
+ ```ruby
268
+ {
269
+ title: "Picker",
270
+ key: :pick,
271
+ type: :picker,
272
+ items: ["Ruby", "Motion", "Rocks"],
273
+ value: "Motion"
274
+ }
275
+ ```
276
+
277
+
278
+ ### <a name="subform"></a> Subform row
279
+ ![Subform row](https://github.com/clayallsopp/formotion/wiki/row-types/subform.gif)
280
+
281
+ ```ruby
282
+ {
283
+ title: "Advanced",
284
+ type: :subform,
285
+ key: :advanced,
286
+ subform: {
287
+ title: "Advanced",
288
+ sections: [{
289
+ title: "Advanced Settings",
290
+ rows: [{
291
+ title: "Server URL",
292
+ key: :server,
293
+ placeholder: "example.com",
294
+ type: :string,
295
+ }]
296
+ }, {
297
+ rows: [{
298
+ title: 'Back',
299
+ type: :back
300
+ }]
301
+ }]
302
+ }
303
+ }
304
+ ```
305
+
306
+ Use a `:display_key` to show the value of the subform in the row:
307
+
308
+ ![Subform row with display](http://i.imgur.com/FoDo1.png)
309
+
310
+ ```ruby
311
+ {
312
+ title: "Subform",
313
+ subtitle: "With display_key",
314
+ type: :subform,
315
+ display_key: :type,
316
+ subform: {
317
+ ...
318
+ }
319
+ }
320
+ ```
321
+
322
+ ### <a name="template"></a> Template row
323
+ ![Template row](https://github.com/clayallsopp/formotion/wiki/row-types/template.gif)
324
+
325
+ ```ruby
326
+ {
327
+ title: 'Your nicknames',
328
+ rows: [{
329
+ title: "Add nickname",
330
+ key: :nicknames,
331
+ type: :template,
332
+ value: ['Nici', 'Sam'],
333
+ template: {
334
+ title: 'Nickname',
335
+ type: :string,
336
+ placeholder: 'Enter here',
337
+ indented: true,
338
+ deletable: true
339
+ }
340
+ }]
150
341
  }
151
342
  ```
152
343
 
@@ -160,4 +351,33 @@
160
351
  title: "Submit",
161
352
  type: :submit,
162
353
  }
163
- ```
354
+ ```
355
+
356
+ The `SubmitRow` triggers the `form.submit` which triggers the defined `on_submit` callback.
357
+
358
+
359
+ ### <a name="back"></a> Back row
360
+ ![Submit row](https://github.com/clayallsopp/formotion/wiki/row-types/back.png)
361
+
362
+ ```ruby
363
+ {
364
+ title: "Back",
365
+ type: :back,
366
+ }
367
+ ```
368
+
369
+ The `BackRow` is used in subforms to go back to the parent form.
370
+
371
+
372
+ ### <a name="edit"></a> Edit row
373
+ ![Submit row](https://github.com/clayallsopp/formotion/wiki/row-types/edit.png)
374
+
375
+ ```ruby
376
+ {
377
+ title: "Edit",
378
+ alt_title: "View",
379
+ type: :edit,
380
+ }
381
+ ```
382
+
383
+ The `EditRow` is used to switch between edit and view mode. It's mainly used for the `TemplateRow`.
data/README.md CHANGED
@@ -109,7 +109,7 @@ self.navigationController.pushViewController(@controller, animated: true)
109
109
 
110
110
  ### Data Types
111
111
 
112
- See [the visual list of support row types](https://github.com/clayallsopp/formotion/blob/master/LIST_OF_ROW_TYPES.md).
112
+ See [the visual list of support row types](https://github.com/clayallsopp/formotion/wiki/List-of-all-the-row-types).
113
113
 
114
114
  To add your own, check [the guide to adding new row types](https://github.com/clayallsopp/formotion/blob/master/NEW_ROW_TYPES.md).
115
115
 
@@ -155,6 +155,25 @@ end
155
155
 
156
156
  `form#submit` just triggers `form#on_submit`.
157
157
 
158
+ ### Persistence
159
+
160
+ You can easily synchronize a `Form`'s state to disk using the `persist_as` key in conjunction with the `persist` method. When your user edits the form, any changes will be immediately saved. For example:
161
+
162
+ ```ruby
163
+ @form = Formotion::Form.persist({
164
+ persist_as: :settings,
165
+ sections: ...
166
+ })
167
+ ```
168
+
169
+ This will load the form if it exists, or create it if necessary. If you use `Formotion::Form.new`, then the form will not be loaded and any changes you make will override existing saved forms.
170
+
171
+ Your form values and state are automatically persisted across application loads, no save buttons needed.
172
+
173
+ To reset the form, `persist` it and call `reset`, which restores it to the original state.
174
+
175
+ View the [Persistence Example](./formotion/tree/master/examples/Persistence) to see it in action.
176
+
158
177
  ## Forking
159
178
 
160
179
  Feel free to fork and submit pull requests! And if you end up using Formotion in your app, I'd love to hear about your experience.
@@ -162,4 +181,4 @@ Feel free to fork and submit pull requests! And if you end up using Formotion in
162
181
  ## Todo
163
182
 
164
183
  - Not very efficient right now (creates a unique reuse idenitifer for each cell)
165
- - Styling/overriding the form for custom UITableViewDelegate/Data Source behaviors.
184
+ - Styling/overriding the form for custom UITableViewDelegate/Data Source behaviors.
@@ -68,6 +68,7 @@ class AppDelegate
68
68
  title: "Account type",
69
69
  type: :subform,
70
70
  key: :account,
71
+ display_key: :type,
71
72
  subform: {
72
73
  title: "Account Type",
73
74
  sections: [{
@@ -75,16 +76,16 @@ class AppDelegate
75
76
  select_one: true,
76
77
  rows: [{
77
78
  title: "Free",
78
- key: :free,
79
+ key: :Free,
79
80
  type: :check,
80
81
  }, {
81
82
  title: "Basic",
82
83
  value: true,
83
- key: :basic,
84
+ key: :Basic,
84
85
  type: :check,
85
86
  }, {
86
87
  title: "Pro",
87
- key: :pro,
88
+ key: :Pro,
88
89
  type: :check,
89
90
  }]
90
91
  }, {
@@ -116,6 +116,73 @@ class AppDelegate
116
116
  key: :c,
117
117
  type: :check,
118
118
  }]
119
+ }, {
120
+ title: "Subforms",
121
+ rows: [{
122
+ title: "Subform",
123
+ subtitle: "With display_key",
124
+ type: :subform,
125
+ key: :subform,
126
+ display_key: :type,
127
+ subform: {
128
+ title: "Account Type",
129
+ sections: [{
130
+ key: :type,
131
+ select_one: true,
132
+ rows: [{
133
+ title: "Free",
134
+ key: :Free,
135
+ type: :check,
136
+ }, {
137
+ title: "Basic",
138
+ value: true,
139
+ key: :Basic,
140
+ type: :check,
141
+ }, {
142
+ title: "Pro",
143
+ key: :Pro,
144
+ type: :check,
145
+ }]
146
+ }, {
147
+ rows: [{
148
+ title: "Advanced",
149
+ type: :subform,
150
+ key: :advanced,
151
+ subform: {
152
+ title: "Advanced",
153
+ sections: [{
154
+ key: :currency,
155
+ select_one: true,
156
+ rows: [{
157
+ title: "USD",
158
+ value: true,
159
+ key: :usd,
160
+ type: :check,
161
+ }, {
162
+ title: "EUR",
163
+ key: :eur,
164
+ type: :check,
165
+ }, {
166
+ title: "CHF",
167
+ key: :chf,
168
+ type: :check,
169
+ }]
170
+ }, {
171
+ rows: [{
172
+ title: 'Back',
173
+ type: :back
174
+ }]
175
+ }]
176
+ }
177
+ }]
178
+ }, {
179
+ rows: [{
180
+ title: 'Back',
181
+ type: :back
182
+ }]
183
+ }]
184
+ }
185
+ }]
119
186
  }, {
120
187
  rows: [{
121
188
  title: "Submit",
@@ -0,0 +1,5 @@
1
+ .repl_history
2
+ build
3
+ resources/*.nib
4
+ resources/*.momd
5
+ resources/*.storyboardc
@@ -0,0 +1,3 @@
1
+ # Persistence example in Formotion
2
+
3
+ ![Persistence](http://i.imgur.com/fdCfV.png)
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ $:.unshift("/Library/RubyMotion/lib")
3
+ require 'motion/project'
4
+ require '../../lib/formotion'
5
+
6
+ Motion::Project::App.setup do |app|
7
+ # Use `rake config' to see complete project settings.
8
+ app.name = 'KitchenSink'
9
+ end
@@ -0,0 +1,199 @@
1
+ class AppDelegate
2
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
3
+ @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
4
+
5
+ @form = Formotion::Form.persist({
6
+ title: "Persist Example",
7
+ persist_as: "example",
8
+ sections: [{
9
+ title: "Section Title",
10
+ footer: "This is the footer for the section. It's good for displaying detailed data about the section.",
11
+ rows: [{
12
+ title: "Static",
13
+ type: :static,
14
+ }, {
15
+ title: "Email",
16
+ key: :email,
17
+ placeholder: "me@mail.com",
18
+ type: :email,
19
+ auto_correction: :no,
20
+ auto_capitalization: :none
21
+ }, {
22
+ title: "Gender",
23
+ key: :gender,
24
+ type: :options,
25
+ items: ['Female', 'Male']
26
+ }, {
27
+ title: "Password",
28
+ key: :password,
29
+ placeholder: "required",
30
+ type: :string,
31
+ secure: true
32
+ }, {
33
+ title: "Phone",
34
+ key: :phone,
35
+ placeholder: "555-555-5555",
36
+ type: :phone,
37
+ auto_correction: :no,
38
+ auto_capitalization: :none
39
+ }, {
40
+ title: "Number",
41
+ key: :number,
42
+ placeholder: "12345",
43
+ type: :number,
44
+ auto_correction: :no,
45
+ auto_capitalization: :none
46
+ }, {
47
+ title: "Subtitle",
48
+ subtitle: "Confirmation",
49
+ key: :confirm,
50
+ placeholder: "required",
51
+ type: :string,
52
+ secure: true
53
+ }, {
54
+ title: "Row Height",
55
+ key: :row_height,
56
+ placeholder: "60px",
57
+ type: :string,
58
+ row_height: 60
59
+ }, {
60
+ title: "Text",
61
+ key: :text,
62
+ type: :text,
63
+ placeholder: "Enter your text here",
64
+ row_height: 100
65
+ }, {
66
+ title: "Check",
67
+ key: :check,
68
+ type: :check,
69
+ value: true
70
+ }, {
71
+ title: "Remember?",
72
+ key: :remember,
73
+ type: :switch,
74
+ }, {
75
+ title: "Date Full",
76
+ subtitle: "w/ :value",
77
+ value: 326937600,
78
+ key: :date_long,
79
+ type: :date,
80
+ format: :full
81
+ }, {
82
+ title: "Date Medium",
83
+ subtitle: "w/ :value",
84
+ value: 1341273600,
85
+ key: :date_medium,
86
+ type: :date,
87
+ format: :medium
88
+ }, {
89
+ title: "Date Short",
90
+ subtitle: "w/ :placeholder",
91
+ placeholder: "DOB",
92
+ key: :date_short,
93
+ type: :date,
94
+ format: :short
95
+ }, {
96
+ title: "Slider",
97
+ key: :slider,
98
+ type: :slider,
99
+ range: (1..100),
100
+ value: 25
101
+ }]
102
+ }, {
103
+ title: "Subforms",
104
+ rows: [{
105
+ title: "Subform",
106
+ subtitle: "With display_key",
107
+ type: :subform,
108
+ key: :subform,
109
+ display_key: :type,
110
+ subform: {
111
+ title: "Account Type",
112
+ sections: [{
113
+ key: :type,
114
+ select_one: true,
115
+ rows: [{
116
+ title: "Free",
117
+ key: :Free,
118
+ type: :check,
119
+ }, {
120
+ title: "Basic",
121
+ value: true,
122
+ key: :Basic,
123
+ type: :check,
124
+ }, {
125
+ title: "Pro",
126
+ key: :Pro,
127
+ type: :check,
128
+ }]
129
+ }, {
130
+ rows: [{
131
+ title: "Advanced",
132
+ type: :subform,
133
+ key: :advanced,
134
+ subform: {
135
+ title: "Advanced",
136
+ sections: [{
137
+ key: :currency,
138
+ select_one: true,
139
+ rows: [{
140
+ title: "USD",
141
+ value: true,
142
+ key: :usd,
143
+ type: :check,
144
+ }, {
145
+ title: "EUR",
146
+ key: :eur,
147
+ type: :check,
148
+ }, {
149
+ title: "CHF",
150
+ key: :chf,
151
+ type: :check,
152
+ }]
153
+ }, {
154
+ rows: [{
155
+ title: 'Back',
156
+ type: :back
157
+ }]
158
+ }]
159
+ }
160
+ }]
161
+ }, {
162
+ rows: [{
163
+ title: 'Back',
164
+ type: :back
165
+ }]
166
+ }]
167
+ }
168
+ }]
169
+ }]
170
+ })
171
+
172
+ @view_controller = Formotion::FormController.alloc.initWithForm(@form)
173
+ @view_controller.form.on_submit do |form|
174
+ form.active_row && form.active_row.text_field.resignFirstResponder
175
+ alert = UIAlertView.alloc.init
176
+ alert.title = "@form.render"
177
+ alert.message = @form.render.to_s
178
+ alert.addButtonWithTitle("OK")
179
+ alert.show
180
+ end
181
+
182
+ @view_controller.navigationItem.leftBarButtonItem = UIBarButtonItem.alloc.initWithTitle("Reset", style: UIBarButtonItemStyleBordered, target:self, action:'reset_form')
183
+
184
+ @navigation_controller = UINavigationController.alloc.initWithRootViewController(@view_controller)
185
+
186
+ @window.rootViewController = @navigation_controller
187
+ @window.makeKeyAndVisible
188
+
189
+ true
190
+ end
191
+
192
+ def submit
193
+ @form.submit
194
+ end
195
+
196
+ def reset_form
197
+ @form.reset
198
+ end
199
+ end
@@ -0,0 +1,9 @@
1
+ describe "Application 'KitchenSink'" do
2
+ before do
3
+ @app = UIApplication.sharedApplication
4
+ end
5
+
6
+ it "has one window" do
7
+ @app.windows.size.should == 1
8
+ end
9
+ end
@@ -114,4 +114,8 @@ hr.hidden-desktop {
114
114
 
115
115
  .install-instruct {
116
116
  margin-bottom: 20px;
117
+ }
118
+
119
+ #maintained {
120
+ margin-bottom: 20px;
117
121
  }
@@ -199,6 +199,22 @@
199
199
 
200
200
  <hr />
201
201
 
202
+ <div class="row">
203
+ <div class="span12" id="maintained">
204
+ <h1>Maintained By</h1>
205
+ </div>
206
+ <div class="span3">
207
+ <a href="https://twitter.com/clayallsopp" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @clayallsopp</a>
208
+ <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
209
+ </div>
210
+ <div class="span3">
211
+ <a href="https://twitter.com/mordaroso" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @mordaroso</a>
212
+ <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
213
+ </div>
214
+ </div>
215
+
216
+ <hr />
217
+
202
218
 
203
219
  </div> <!-- /container -->
204
220
  </body>
@@ -17,6 +17,7 @@ module Formotion
17
17
  self.form = form
18
18
  #self.view.setEditing true, animated: true
19
19
  self.tableView.allowsSelectionDuringEditing = true
20
+
20
21
  self
21
22
  end
22
23
 
@@ -49,6 +50,13 @@ module Formotion
49
50
  @form.controller = self
50
51
  end
51
52
 
53
+
54
+ def viewWillAppear(animated)
55
+ super
56
+
57
+ self.tableView.reloadData
58
+ end
59
+
52
60
  # Subview Methods
53
61
  def push_subform(form)
54
62
  @subform_controller = self.class.alloc.initWithForm(form)
@@ -68,4 +76,4 @@ module Formotion
68
76
  end
69
77
  end
70
78
  end
71
- end
79
+ end
@@ -1,11 +1,16 @@
1
1
  module Formotion
2
2
  class Form < Formotion::Base
3
+ extend BW::KVO
4
+ include BW::KVO
5
+
3
6
  PROPERTIES = [
4
7
  # By default, Formotion::Controller will set it's title to this
5
8
  # (so navigation bars will reflect it).
6
9
  :title,
7
10
  # If you want to have some internal id to track the form.
8
- :id
11
+ :id,
12
+ # [STRING/SYMBOL] used to store your form's state on disk
13
+ :persist_as
9
14
  ]
10
15
  PROPERTIES.each {|prop|
11
16
  attr_accessor prop
@@ -15,6 +20,12 @@ module Formotion
15
20
  # them to be pased in the hash
16
21
  SERIALIZE_PROPERTIES = PROPERTIES + [:sections]
17
22
 
23
+ def self.persist(params = {})
24
+ form = new(params)
25
+ form.open && form.save
26
+ form
27
+ end
28
+
18
29
  def initialize(params = {})
19
30
  # super takes care of initializing the ::PROPERTIES in params
20
31
  super
@@ -104,7 +115,6 @@ module Formotion
104
115
  # as shown above.
105
116
  def submit
106
117
  if @on_submit_callback.nil?
107
- p "`Form#submit` invoked without a callback, doing nothing."
108
118
  return
109
119
  end
110
120
 
@@ -184,7 +194,95 @@ module Formotion
184
194
  kv
185
195
  end
186
196
 
197
+ #########################
198
+ # Persisting Forms
199
+
200
+ # loads the given settings into the the form, and
201
+ # places observers to save on changes
202
+ def open
203
+ @form_observer ||= lambda { |form, saved_form|
204
+ form.send(:each_row) do |row, index|
205
+ s_index = row.section.index
206
+ temp_row = saved_form.sections[s_index].rows[index]
207
+
208
+ if row.subform?
209
+ saved_subform = temp_row.subform.to_form
210
+ @form_observer.call(row.subform.to_form, saved_subform)
211
+ else
212
+ row.value = temp_row.value
213
+
214
+ observe(row, "value") do |old_value, new_value|
215
+ self.save
216
+ end
217
+ end
218
+ end
219
+ }
220
+
221
+ saved_hash = load_state
222
+ @form_observer.call(self, Formotion::Form.new(saved_hash))
223
+ end
224
+
225
+ # places hash of values into application persistance
226
+ def save
227
+ App::Persistence[persist_key] = to_hash.to_archived_data
228
+ App::Persistence[original_persist_key] ||= self.to_hash.to_archived_data
229
+ end
230
+
231
+ def reset
232
+ App::Persistence[persist_key] = nil
233
+
234
+ @form_resetter ||= lambda { |form, original_form|
235
+ form.send(:each_row) do |row, index|
236
+ s_index = row.section.index
237
+ temp_row = original_form.sections[s_index].rows[index]
238
+
239
+ if row.subform?
240
+ original_subform = temp_row.subform.to_form
241
+ @form_resetter.call(row.subform.to_form, original_subform)
242
+ else
243
+ row.value = temp_row.value
244
+ end
245
+ end
246
+ }
247
+
248
+ temp_form = Formotion::Form.new(App::Persistence[original_persist_key].unarchive)
249
+ @form_resetter.call(self, temp_form)
250
+
251
+ self.save
252
+ end
253
+
187
254
  private
255
+ def persist_key
256
+ "FORMOTION_#{self.persist_as}"
257
+ end
258
+
259
+ def original_persist_key
260
+ "#{persist_key}_ORIGINAL"
261
+ end
262
+
263
+ def load_state
264
+ begin
265
+ state = App::Persistence[persist_key] && App::Persistence[persist_key].unarchive
266
+ rescue
267
+ self.save
268
+ end
269
+
270
+ state ||= self.to_hash
271
+ end
272
+
273
+ def each_row(&block)
274
+ self.sections.each_with_index do |section, s_index|
275
+ section.rows.each_with_index do |row, index|
276
+ case block.arity
277
+ when 1
278
+ block.call(row)
279
+ when 2
280
+ block.call(row, index)
281
+ end
282
+ end
283
+ end
284
+ end
285
+
188
286
  def recursive_delete_nil(h)
189
287
  delete_empty = Proc.new { |k, v|
190
288
  v.delete_if(&delete_empty) if v.kind_of?(Hash)
@@ -193,4 +291,4 @@ module Formotion
193
291
  h.delete_if &delete_empty
194
292
  end
195
293
  end
196
- end
294
+ end
@@ -65,6 +65,8 @@ module Formotion
65
65
  row.make_cell
66
66
  end
67
67
 
68
+ row.update_cell(cell)
69
+
68
70
  cell
69
71
  end
70
72
 
@@ -5,6 +5,21 @@ module Formotion
5
5
  end
6
6
 
7
7
  module ClassMethods
8
+ INHERITABLE_ATTRIBUTES = [:form_properties, :form_title].each do |prop|
9
+ attr_accessor prop
10
+ end
11
+
12
+ def inherited(subclass)
13
+ INHERITABLE_ATTRIBUTES.each do |inheritable_attribute|
14
+ instance_var = "@#{inheritable_attribute}"
15
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
16
+ end
17
+ end
18
+
19
+ def form_properties
20
+ @form_properties ||= []
21
+ end
22
+
8
23
  # Relates a property to a RowType.
9
24
  # @param property is the name of the attribute to KVO
10
25
  # @param row_type is the Formotion::RowType to use for that attribute
@@ -19,10 +34,6 @@ module Formotion
19
34
  self.form_properties << { property: property, row_type: row_type}.merge(options)
20
35
  end
21
36
 
22
- def form_properties
23
- @form_properties ||= []
24
- end
25
-
26
37
  # Sets the top bar title for this model
27
38
  # EX
28
39
  # form_title "Some Settings"
@@ -6,4 +6,14 @@ class Object
6
6
  self.class.send(:alias_method, "old_#{method.to_s}".to_sym, method)
7
7
  self.instance_eval &block
8
8
  end
9
- end
9
+
10
+ def to_archived_data
11
+ NSKeyedArchiver.archivedDataWithRootObject(self)
12
+ end
13
+ end
14
+
15
+ class NSData
16
+ def unarchive
17
+ NSKeyedUnarchiver.unarchiveObjectWithData(self)
18
+ end
19
+ end
@@ -19,7 +19,13 @@ class UITextView
19
19
  def setup
20
20
  @foreground_observer = NSNotificationCenter.defaultCenter.observe UITextViewTextDidChangeNotification do |notification|
21
21
  updateShouldDrawPlaceholder
22
- end
22
+ end
23
+ end
24
+
25
+ alias_method :old_dealloc, :dealloc
26
+ def dealloc
27
+ NSNotificationCenter.defaultCenter.unobserve @foreground_observer
28
+ old_dealloc
23
29
  end
24
30
 
25
31
  alias_method :old_drawRect, :drawRect
@@ -62,4 +68,4 @@ class UITextView
62
68
 
63
69
  self.setNeedsDisplay if (prev != @shouldDrawPlaceholder)
64
70
  end
65
- end
71
+ end
@@ -53,6 +53,11 @@ module Formotion
53
53
  # A hash for a Form used for subforms
54
54
  # DEFAULT is nil
55
55
  :subform,
56
+ # Used in a subform row; when given,
57
+ # will display the value of the matching key
58
+ # of the subform's rendering.
59
+ # DEFAULT is nil
60
+ :display_key,
56
61
  # A hash for a Row used for templates
57
62
  # DEFAULT is nil
58
63
  :template,
@@ -160,6 +165,19 @@ module Formotion
160
165
  object.button?
161
166
  end
162
167
 
168
+ def subform?
169
+ self.type == :subform
170
+ end
171
+
172
+ #########################
173
+ # getter overrides
174
+ def items
175
+ if @items.respond_to?(:call)
176
+ @items = @items.call
177
+ end
178
+ @items
179
+ end
180
+
163
181
  #########################
164
182
  # setter overrides
165
183
  def type=(type)
@@ -167,6 +185,20 @@ module Formotion
167
185
  @type = type
168
186
  end
169
187
 
188
+ def range=(range)
189
+ if range
190
+ case range
191
+ when Range
192
+ # all good
193
+ when Array
194
+ range = Range.new(range[0], range[1])
195
+ else
196
+ raise Formotion::InvalidClassError, "Attempted Row.range = #{range.inspect} should be of type Range or Array"
197
+ end
198
+ end
199
+ @range = range
200
+ end
201
+
170
202
  def return_key=(value)
171
203
  @return_key = const_int_get("UIReturnKey", value)
172
204
  end
@@ -205,10 +237,24 @@ module Formotion
205
237
  cell
206
238
  end
207
239
 
240
+ # Called on every tableView:cellForRowAtIndexPath:
241
+ # so keep implementation details minimal
242
+ def update_cell(cell)
243
+ self.object.update_cell(cell)
244
+ cell
245
+ end
246
+
208
247
  #########################
209
248
  # Retreiving data
210
249
  def to_hash
211
- super
250
+ h = super
251
+ if h[:range] && h[:range].is_a?(Range)
252
+ h[:range] = [self.range.begin, self.range.end]
253
+ end
254
+ if subform?
255
+ h[:subform] = self.subform.to_form.to_hash
256
+ end
257
+ h
212
258
  end
213
259
 
214
260
  def subform=(subform)
@@ -225,6 +271,7 @@ module Formotion
225
271
  @hash_subform ||= self
226
272
  end
227
273
  end
274
+ @subform
228
275
  end
229
276
 
230
277
  private
@@ -43,6 +43,11 @@ module Formotion
43
43
  def after_build(cell)
44
44
  end
45
45
 
46
+ # Called on every tableView:cellForRowAtIndexPath:
47
+ # so keep implementation details minimal
48
+ def update_cell(cell)
49
+ end
50
+
46
51
  # method gets triggered when tableView(tableView, didSelectRowAtIndexPath:indexPath)
47
52
  # in UITableViewDelegate is executed
48
53
  def on_select(tableView, tableViewDelegate)
@@ -18,7 +18,11 @@ module Formotion
18
18
 
19
19
  if self.row.value
20
20
  picker_row = self.row.items.index(row.value)
21
- picker.selectRow(picker_row, inComponent:0, animated:false)
21
+ if picker_row
22
+ picker.selectRow(picker_row, inComponent:0, animated:false)
23
+ else
24
+ warn "Picker item '#{row.value}' not found in #{row.items.inspect} for '#{row.key}'"
25
+ end
22
26
  end
23
27
 
24
28
  picker
@@ -2,8 +2,36 @@ module Formotion
2
2
  module RowType
3
3
  class SubformRow < Base
4
4
 
5
+ LABEL_TAG=1001
6
+
5
7
  def build_cell(cell)
6
8
  cell.accessoryType = cell.editingAccessoryType = UITableViewCellAccessoryDisclosureIndicator
9
+
10
+ cell.contentView.addSubview(self.display_key_label)
11
+
12
+ cell.swizzle(:layoutSubviews) do
13
+ def layoutSubviews
14
+ old_layoutSubviews
15
+
16
+ formotion_label = self.viewWithTag(LABEL_TAG)
17
+ formotion_label.sizeToFit
18
+
19
+ field_frame = formotion_label.frame
20
+ # HARDCODED CONSTANT
21
+ field_frame.origin.x = self.contentView.frame.size.width - field_frame.size.width - 10
22
+ field_frame.origin.y = ((self.contentView.frame.size.height - field_frame.size.height) / 2.0).round
23
+ formotion_label.frame = field_frame
24
+ end
25
+ end
26
+
27
+ display_key_label.highlightedTextColor = cell.textLabel.highlightedTextColor
28
+ end
29
+
30
+ def update_cell(cell)
31
+ subform = row.subform.to_form
32
+ if row.display_key and subform.render[row.display_key]
33
+ self.display_key_label.text = subform.render[row.display_key]
34
+ end
7
35
  end
8
36
 
9
37
  def on_select(tableView, tableViewDelegate)
@@ -11,6 +39,15 @@ module Formotion
11
39
  row.form.controller.push_subform(subform)
12
40
  end
13
41
 
42
+ def display_key_label
43
+ @display_key_label ||= begin
44
+ label = UILabel.alloc.initWithFrame(CGRectZero)
45
+ label.textColor = "#385387".to_color
46
+ label.tag = LABEL_TAG
47
+ label.backgroundColor = UIColor.clearColor
48
+ label
49
+ end
50
+ end
14
51
  end
15
52
  end
16
53
  end
@@ -1,3 +1,3 @@
1
1
  module Formotion
2
- VERSION = "1.0"
2
+ VERSION = "1.1"
3
3
  end
@@ -0,0 +1,58 @@
1
+ describe "Form Persisting" do
2
+
3
+ it "works" do
4
+ f = Formotion::Form.persist({
5
+ persist_as: "test_#{rand(255)}",
6
+ sections: [
7
+ rows: [ {
8
+ key: "first",
9
+ type: "string",
10
+ value: "initial value"
11
+ }
12
+ ]
13
+ ]
14
+ })
15
+
16
+ r = f.sections[0].rows[0]
17
+ r.value = "new value"
18
+
19
+ saved = Formotion::Form.new(f.send(:load_state))
20
+ saved.sections[0].rows[0].value.should == r.value
21
+
22
+ f.reset
23
+ r.value.should == "initial value"
24
+ end
25
+
26
+ it "works with subforms" do
27
+ f = Formotion::Form.persist({
28
+ persist_as: "test_#{rand(255)}",
29
+ sections: [
30
+ rows: [ {
31
+ key: :subform,
32
+ type: :subform,
33
+ title: "Subform",
34
+ subform: {
35
+ title: "New Page",
36
+ sections: [
37
+ rows: [{
38
+ key: "second",
39
+ type: "string",
40
+ value: "initial value"
41
+ }]
42
+ ]
43
+ }
44
+ }
45
+ ]
46
+ ]
47
+ })
48
+
49
+ r = f.sections[0].rows[0].subform.to_form.sections[0].rows[0]
50
+ r.value = "new value"
51
+
52
+ saved = Formotion::Form.new(f.send(:load_state))
53
+ saved.sections[0].rows[0].subform.to_form.sections[0].rows[0].value.should == r.value
54
+
55
+ f.reset
56
+ r.value.should == "initial value"
57
+ end
58
+ end
@@ -7,6 +7,15 @@ class TestModel
7
7
  form_property :my_number, :number, title: "My Number", :transform => lambda { |value| value.to_i + 10 }
8
8
  end
9
9
 
10
+ class AdditionalTestModel
11
+ include Formotion::Formable
12
+
13
+ attr_accessor :my_other_name, :my_other_number
14
+
15
+ form_property :my_other_name, :string
16
+ form_property :my_other_number, :number
17
+ end
18
+
10
19
  describe "FormableController" do
11
20
  tests Formotion::FormableController
12
21
 
@@ -22,4 +22,13 @@ describe "Rows" do
22
22
  r.title?.should == true
23
23
  }
24
24
  end
25
+
26
+ it "should allow arrays and blocks for items" do
27
+ r = Formotion::Row.new()
28
+ r.items = [1, 2, 3]
29
+ r.items.should == [1, 2, 3]
30
+
31
+ r.items = lambda { [1, 2, 3].reverse }
32
+ r.items.should == [3, 2, 1]
33
+ end
25
34
  end
@@ -1,7 +1,9 @@
1
1
  module UIControlWrap
2
- # adds ability to trigger a defined callback that was previously defined by when
2
+ # adds ability to trigger a defined callback
3
+ # that was previously defined by #when
3
4
  def trigger(events)
4
5
  @callback ||= {}
5
- @callback[events].call if @callback.has_key? events
6
+ p "Callback #{@callback}"
7
+ @callback[events].map(&:call) if @callback.has_key? events
6
8
  end
7
9
  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.0'
4
+ version: '1.1'
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: 2012-08-12 00:00:00.000000000 Z
12
+ date: 2012-09-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bubble-wrap
@@ -77,6 +77,11 @@ files:
77
77
  - examples/Login/app/login_controller.rb
78
78
  - examples/Login/app/user_controller.rb
79
79
  - examples/Login/spec/main_spec.rb
80
+ - examples/Persistence/.gitignore
81
+ - examples/Persistence/README.md
82
+ - examples/Persistence/Rakefile
83
+ - examples/Persistence/app/app_delegate.rb
84
+ - examples/Persistence/spec/main_spec.rb
80
85
  - gh-pages/assets/css/bootstrap-responsive.css
81
86
  - gh-pages/assets/css/bootstrap-responsive.min.css
82
87
  - gh-pages/assets/css/bootstrap.css
@@ -138,6 +143,7 @@ files:
138
143
  - lib/formotion/row_type/text_row.rb
139
144
  - lib/formotion/section/section.rb
140
145
  - lib/formotion/version.rb
146
+ - spec/form/persist_spec.rb
141
147
  - spec/form_spec.rb
142
148
  - spec/formable_spec.rb
143
149
  - spec/functional/character_spec.rb
@@ -188,7 +194,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
188
194
  version: '0'
189
195
  segments:
190
196
  - 0
191
- hash: 84446603769905079
197
+ hash: 4038020492425824062
192
198
  required_rubygems_version: !ruby/object:Gem::Requirement
193
199
  none: false
194
200
  requirements:
@@ -197,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
203
  version: '0'
198
204
  segments:
199
205
  - 0
200
- hash: 84446603769905079
206
+ hash: 4038020492425824062
201
207
  requirements: []
202
208
  rubyforge_project:
203
209
  rubygems_version: 1.8.23
@@ -205,6 +211,7 @@ signing_key:
205
211
  specification_version: 3
206
212
  summary: Making iOS Forms insanely great with RubyMotion
207
213
  test_files:
214
+ - spec/form/persist_spec.rb
208
215
  - spec/form_spec.rb
209
216
  - spec/formable_spec.rb
210
217
  - spec/functional/character_spec.rb