formotion 0.0.1

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.
@@ -0,0 +1,20 @@
1
+ module Formotion
2
+ class InvalidClassError < StandardError; end
3
+ class InvalidSectionError < StandardError; end
4
+
5
+ class Conditions
6
+ class << self
7
+ def assert_nil_or_boolean(obj)
8
+ if not (obj.nil? or obj.is_a? TrueClass or obj.is_a? FalseClass)
9
+ raise Formotion::InvalidClassError, "#{obj.inspect} should be nil, true, or false, but is #{obj.class.to_s}"
10
+ end
11
+ end
12
+
13
+ def assert_class(obj, klass)
14
+ if not obj.is_a? klass
15
+ raise Formotion::InvalidClassError, "#{obj.inspect} of class #{obj.class.to_s} is not of class #{klass.to_s}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,173 @@
1
+ module Formotion
2
+ class Form < Formotion::Base
3
+ PROPERTIES = [
4
+ # By default, Formotion::Controller will set it's title to this
5
+ # (so navigation bars will reflect it).
6
+ :title,
7
+ # If you want to have some internal id to track the form.
8
+ :id
9
+ ]
10
+ PROPERTIES.each {|prop|
11
+ attr_accessor prop
12
+ }
13
+
14
+ # Sections are create specially using #create_section, so we don't allow
15
+ # them to be pased in the hash
16
+ SERIALIZE_PROPERTIES = PROPERTIES + [:sections]
17
+
18
+ def initialize(params = {})
19
+ # super takes care of initializing the ::PROPERTIES in params
20
+ super
21
+
22
+ sections = params[:sections] || params["sections"]
23
+ sections && sections.each_with_index {|section_hash, index|
24
+ section = create_section(section_hash.merge({index: index}))
25
+ }
26
+ end
27
+
28
+ # Use this as a DSL for building forms
29
+ # EX
30
+ # @form = Form.build do |form|
31
+ # form.title = 'Form Title'
32
+ # form.id = 'anything'
33
+ # end
34
+ class << self
35
+ def build(&block)
36
+ form = new
37
+ block.call(form)
38
+ form
39
+ end
40
+ end
41
+
42
+ # Use this as a DSL for adding sections
43
+ # EX
44
+ # @form.build_section do |section|
45
+ # section.title = 'Section Title'
46
+ # end
47
+ def build_section(&block)
48
+ section = create_section
49
+ block.call(section)
50
+ section
51
+ end
52
+
53
+ # Use this to add sections via a hash
54
+ # EX
55
+ # @form.create_section(:title => 'Section Title')
56
+ def create_section(hash = {})
57
+ section = Formotion::Section.new(hash)
58
+ section.form = self
59
+ section.index = self.sections.count
60
+ self.sections << section
61
+ section
62
+ end
63
+
64
+ #########################
65
+ # attributes
66
+
67
+ def sections
68
+ @sections ||= []
69
+ end
70
+
71
+ def sections=(sections)
72
+ sections.each {|section|
73
+ Formotion::Conditions.assert_class(section, Formotion::Section)
74
+ }
75
+ @sections = sections
76
+ end
77
+
78
+ # Accepts an NSIndexPath and gives back a Formotion::Row
79
+ # EX
80
+ # row = @form.row_for_index_path(NSIndexPath.indexPathForRow(0, inSection: 0))
81
+ def row_for_index_path(index_path)
82
+ self.sections[index_path.section].rows[index_path.row]
83
+ end
84
+
85
+ #########################
86
+ # callbacks
87
+
88
+ # Stores the callback block when you do #submit.
89
+ # EX
90
+ # @form.on_submit do
91
+ # do_something(@form.render)
92
+ # end
93
+ #
94
+ # EX
95
+ # @form.on_submit do |form|
96
+ # pass_to_server(form.render)
97
+ # end
98
+ def on_submit(&block)
99
+ @on_submit_callback = block
100
+ end
101
+
102
+ # Triggers the #on_submit block
103
+ # Handles either zero or one arguments,
104
+ # as shown above.
105
+ def submit
106
+ if @on_submit_callback.arity == 0
107
+ @on_submit_callback.call
108
+ elsif @on_submit_callback.arity == 1
109
+ @on_submit_callback.call(self)
110
+ end
111
+ end
112
+
113
+ #########################
114
+ # Retreiving data
115
+
116
+ # A complete hashification of the Form
117
+ # EX
118
+ # @form = Formotion::Form.new(title: 'My Title')
119
+ # @form.id = 'anything'
120
+ # @form.to_hash
121
+ # => {title: 'My Title', id: 'anything'}
122
+ def to_hash
123
+ # super handles all of the ::PROPERTIES
124
+ h = super
125
+ h[:sections] = self.sections.collect { |section|
126
+ section.to_hash
127
+ }
128
+ recursive_delete_nil(h)
129
+ h
130
+ end
131
+
132
+ # A hashification with the user's inputted values
133
+ # and row keys.
134
+ # EX
135
+ # @form = Formotion::Form.new(sections: [{
136
+ # rows: [{
137
+ # key: 'Email',
138
+ # editable: true,
139
+ # title: 'Email'
140
+ # }]}])
141
+ # ...user plays with the Form...
142
+ # @form.render
143
+ # => {email: 'something@email.com'}
144
+ def render
145
+ kv = {}
146
+ self.sections.each {|section|
147
+ if section.select_one?
148
+ section.rows.each {|row|
149
+ if row.value
150
+ kv[section.key] = row.key
151
+ end
152
+ }
153
+ else
154
+ section.rows.each {|row|
155
+ next if row.submit_button?
156
+ kv[row.key] = row.value
157
+ }
158
+ end
159
+ }
160
+ kv.delete_if {|k, v| k.nil? }
161
+ kv
162
+ end
163
+
164
+ private
165
+ def recursive_delete_nil(h)
166
+ delete_empty = Proc.new { |k, v|
167
+ v.delete_if(&delete_empty) if v.kind_of?(Hash)
168
+ v.nil?
169
+ }
170
+ h.delete_if &delete_empty
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,102 @@
1
+ #################
2
+ #
3
+ # Formotion::FormController
4
+ # Use #initWithForm to create a view controller
5
+ # loaded with your form.
6
+ #
7
+ #################
8
+ module Formotion
9
+ class FormController < UIViewController
10
+ attr_accessor :form
11
+ attr_reader :table_view
12
+
13
+ # Initializes controller with a form
14
+ # PARAMS form.is_a? [Hash, Formotion::Form]
15
+ # RETURNS An instance of Formotion::FormController
16
+ def initWithForm(form)
17
+ self.initWithNibName(nil, bundle: nil)
18
+ self.form = form
19
+ self
20
+ end
21
+
22
+ # Set the form; ensure it is/can be converted to Formotion::Form
23
+ # or raises an exception.
24
+ def form=(form)
25
+ if form.is_a? Hash
26
+ form = Formotion::Form.new(form)
27
+ elsif not form.is_a? Formotion::Form
28
+ raise Formotion::InvalidClassError, "Attempted FormController.form = #{form.inspect} should be of type Formotion::Form or Hash"
29
+ end
30
+ @form = form
31
+ end
32
+
33
+ def viewDidLoad
34
+ super
35
+
36
+ # via https://gist.github.com/330916, could be wrong.
37
+ tabBarHeight = self.tabBarController && self.tabBarController.tabBar.bounds.size.height
38
+ tabBarHeight ||= 0
39
+ navBarHeight = self.navigationController && (self.navigationController.isNavigationBarHidden ? 0.0 : self.navigationController.navigationBar.bounds.size.height)
40
+ navBarHeight ||= 0
41
+ frame = self.view.frame
42
+ frame.size.height = frame.size.height - navBarHeight - tabBarHeight
43
+ self.view.frame = frame
44
+
45
+ self.title = self.form.title
46
+
47
+ NSNotificationCenter.defaultCenter.addObserver(self, selector:'keyboardWillHideOrShow:', name:UIKeyboardWillHideNotification, object:nil);
48
+ NSNotificationCenter.defaultCenter.addObserver(self, selector:'keyboardWillHideOrShow:', name:UIKeyboardWillShowNotification, object:nil);
49
+
50
+ @table_view = UITableView.alloc.initWithFrame(self.view.bounds, style: UITableViewStyleGrouped)
51
+ self.view.addSubview @table_view
52
+
53
+ # Triggers this block when the enter key is pressed
54
+ # while editing the last text field.
55
+ @form.sections[-1] && @form.sections[-1].rows[-1].on_enter do |row|
56
+ if row.text_field
57
+ @form.submit
58
+ row.text_field.resignFirstResponder
59
+ end
60
+ end
61
+
62
+ # Setting @form.controller assigns
63
+ # @form as the datasource and delegate
64
+ # and reloads the data.
65
+ @form.controller = self
66
+ end
67
+
68
+ # This re-sizes + scrolls the tableview to account for the keyboard size.
69
+ # TODO: Test this on iPads, etc.
70
+ def keyboardWillHideOrShow(note)
71
+ last_note = @keyboard_state
72
+ @keyboard_state = note.name
73
+ if last_note == @keyboard_state
74
+ return
75
+ end
76
+
77
+ userInfo = note.userInfo
78
+ duration = userInfo[UIKeyboardAnimationDurationUserInfoKey].doubleValue
79
+ curve = userInfo[UIKeyboardAnimationCurveUserInfoKey].intValue
80
+ keyboardFrame = userInfo[UIKeyboardFrameEndUserInfoKey].CGRectValue
81
+
82
+ view_frame = @table_view.frame;
83
+
84
+ if @keyboard_state == UIKeyboardWillHideNotification
85
+ view_frame.size.height = self.view.bounds.size.height
86
+ else
87
+ view_frame.size.height -= keyboardFrame.size.height
88
+ end
89
+
90
+ UIView.beginAnimations(nil, context: nil)
91
+ UIView.setAnimationDuration(duration)
92
+ UIView.setAnimationDelay(0)
93
+ UIView.setAnimationCurve(curve)
94
+ UIView.setAnimationBeginsFromCurrentState(true)
95
+
96
+ @table_view.frame = view_frame
97
+ @form.active_row = @form.active_row
98
+
99
+ UIView.commitAnimations
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,87 @@
1
+ module Formotion
2
+ class Form
3
+ attr_reader :table
4
+ attr_reader :controller
5
+ attr_reader :active_row
6
+
7
+ def active_row=(row)
8
+ @active_row = row
9
+
10
+ if @active_row && @table
11
+ index_path = NSIndexPath.indexPathForRow(@active_row.index, inSection:@active_row.section.index)
12
+ @table.scrollToRowAtIndexPath(index_path, atScrollPosition:UITableViewScrollPositionMiddle, animated:true)
13
+ end
14
+ end
15
+
16
+ ####################
17
+ # Table Methods
18
+ def controller=(controller)
19
+ @controller = controller
20
+ self.table = controller.table_view
21
+ end
22
+
23
+ def table=(table_view)
24
+ @table = table_view
25
+
26
+ @table.delegate = self
27
+ @table.dataSource = self
28
+ reload_data
29
+ end
30
+
31
+ def reload_data
32
+ previous_row, next_row = nil
33
+
34
+ last_row = self.sections[-1].rows[-1]
35
+ if last_row
36
+ last_row.return_key ||= UIReturnKeyDone
37
+ end
38
+
39
+ @table.reloadData
40
+ end
41
+
42
+ # UITableViewDataSource Methods
43
+ def numberOfSectionsInTableView(tableView)
44
+ self.sections.count
45
+ end
46
+
47
+ def tableView(tableView, numberOfRowsInSection: section)
48
+ self.sections[section].rows.count
49
+ end
50
+
51
+ def tableView(tableView, titleForHeaderInSection:section)
52
+ section = self.sections[section].title
53
+ end
54
+
55
+ def tableView(tableView, cellForRowAtIndexPath:indexPath)
56
+ row = row_for_index_path(indexPath)
57
+ reuseIdentifier = row.reuse_identifier
58
+
59
+ cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier) || begin
60
+ row.make_cell
61
+ end
62
+
63
+ cell
64
+ end
65
+
66
+ # UITableViewDelegate Methods
67
+ def tableView(tableView, didSelectRowAtIndexPath:indexPath)
68
+ tableView.deselectRowAtIndexPath(indexPath, animated:true)
69
+ row = row_for_index_path(indexPath)
70
+ if row.submit_button?
71
+ self.submit
72
+ elsif row.checkable?
73
+ if row.section.select_one and !row.value
74
+ row.section.rows.each {|other_row|
75
+ other_row.value = (other_row == row)
76
+ Formotion::RowCellBuilder.make_check_cell(other_row, tableView.cellForRowAtIndexPath(other_row.index_path))
77
+ }
78
+ elsif !row.section.select_one
79
+ row.value = !row.value
80
+ Formotion::RowCellBuilder.make_check_cell(row, tableView.cellForRowAtIndexPath(row.index_path))
81
+ end
82
+ elsif row.editable?
83
+ row.text_field.becomeFirstResponder
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,158 @@
1
+ # Methods which use blocks for UITextFieldDelegate methods.
2
+ # EX
3
+ # field.should_end? do |text_field|
4
+ # if text_field.text != "secret"
5
+ # return false
6
+ # end
7
+ # true
8
+ # end
9
+ #
10
+ # Also includes an on_change method, which calls after the text
11
+ # has changed (there is no UITextFieldDelegate equivalent.)
12
+ # EX
13
+ # field.on_change do |text_field|
14
+ # p text_field.text
15
+ # end
16
+
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
+ # block takes argument textField; should return true/false
25
+ def should_begin?(&block)
26
+ add_delegate_method do
27
+ @delegate.textFieldShouldBeginEditing_callback = block
28
+ end
29
+ end
30
+
31
+ # block takes argument textField
32
+ def on_begin(&block)
33
+ add_delegate_method do
34
+ @delegate.textFieldDidBeginEditing_callback = block
35
+ end
36
+ end
37
+
38
+ # block takes argument textField; should return true/false
39
+ def should_end?(&block)
40
+ add_delegate_method do
41
+ @delegate.textFieldShouldEndEditing_callback = block
42
+ end
43
+ end
44
+
45
+ # block takes argument textField
46
+ def on_end(&block)
47
+ add_delegate_method do
48
+ @delegate.textFieldDidBeginEditing_callback = block
49
+ end
50
+ end
51
+
52
+ # block takes argument textField, range [NSRange], and string; should return true/false
53
+ def should_change?(&block)
54
+ add_delegate_method do
55
+ @delegate.shouldChangeCharactersInRange_callback = block
56
+ end
57
+ end
58
+
59
+ # block takes argument textField
60
+ def on_change(&block)
61
+ add_delegate_method do
62
+ @delegate.on_change_callback = block
63
+ self.addTarget(@delegate, action: 'on_change:', forControlEvents: UIControlEventEditingChanged)
64
+ end
65
+ end
66
+
67
+ # block takes argument textField; should return true/false
68
+ def should_clear?(&block)
69
+ add_delegate_method do
70
+ @delegate.textFieldShouldClear_callback = block
71
+ end
72
+ end
73
+
74
+ # block takes argument textField; should return true/false
75
+ def should_return?(&block)
76
+ add_delegate_method do
77
+ @delegate.textFieldShouldReturn_callback = block
78
+ end
79
+ end
80
+
81
+ private
82
+ def add_delegate_method
83
+ # create strong reference to the delegate
84
+ # (.delegate= only creates a weak reference)
85
+ @delegate ||= UITextField_Delegate.new
86
+ yield
87
+ self.delegate = @delegate
88
+ end
89
+ end
90
+
91
+ class UITextField_Delegate
92
+ [:textFieldShouldBeginEditing, :textFieldDidBeginEditing,
93
+ :textFieldShouldEndEditing, :textFieldDidEndEditing,
94
+ :shouldChangeCharactersInRange, :textFieldShouldClear,
95
+ :textFieldShouldReturn].each {|method|
96
+ attr_accessor (method.to_s + "_callback").to_sym
97
+ }
98
+
99
+ # Called from
100
+ # [textField addTarget:block
101
+ # action:'call'
102
+ # forControlEvents:UIControlEventEditingChanged],
103
+ # NOT a UITextFieldDelegate method.
104
+ attr_accessor :on_change_callback
105
+
106
+ def textFieldShouldBeginEditing(theTextField)
107
+ if self.textFieldShouldBeginEditing_callback
108
+ return self.textFieldShouldBeginEditing_callback.call(theTextField)
109
+ end
110
+ true
111
+ end
112
+
113
+ def textFieldDidBeginEditing(theTextField)
114
+ if self.textFieldDidBeginEditing_callback
115
+ return self.textFieldDidBeginEditing_callback.call(theTextField)
116
+ end
117
+ end
118
+
119
+ def textFieldShouldEndEditing(theTextField)
120
+ if self.textFieldShouldEndEditing_callback
121
+ return self.textFieldShouldEndEditing_callback.call(theTextField)
122
+ end
123
+ true
124
+ end
125
+
126
+ def textFieldDidEndEditing(theTextField)
127
+ if self.textFieldDidEndEditing_callback
128
+ return self.textFieldDidEndEditing_callback.call(theTextField)
129
+ end
130
+ end
131
+
132
+ def textField(theTextField, shouldChangeCharactersInRange:range, replacementString:string)
133
+ if self.shouldChangeCharactersInRange_callback
134
+ return self.shouldChangeCharactersInRange_callback.call(theTextField, range, string)
135
+ end
136
+ true
137
+ end
138
+
139
+ def on_change(theTextField)
140
+ if self.on_change_callback
141
+ self.on_change_callback.call(theTextField)
142
+ end
143
+ end
144
+
145
+ def textFieldShouldClear(theTextField)
146
+ if self.textFieldShouldClear_callback
147
+ return self.textFieldShouldClear_callback.call(theTextField)
148
+ end
149
+ true
150
+ end
151
+
152
+ def textFieldShouldReturn(theTextField)
153
+ if self.textFieldShouldReturn_callback
154
+ return self.textFieldShouldReturn_callback.call(theTextField)
155
+ end
156
+ true
157
+ end
158
+ end