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.
- data/.gitignore +10 -0
- data/Formotion.gemspec +19 -0
- data/README.md +168 -0
- data/Rakefile +11 -0
- data/app/app_delegate.rb +68 -0
- data/examples/KitchenSink/.gitignore +5 -0
- data/examples/KitchenSink/README.md +5 -0
- data/examples/KitchenSink/Rakefile +9 -0
- data/examples/KitchenSink/app/app_delegate.rb +101 -0
- data/examples/KitchenSink/spec/main_spec.rb +9 -0
- data/lib/formotion.rb +6 -0
- data/lib/formotion/base.rb +44 -0
- data/lib/formotion/exceptions.rb +20 -0
- data/lib/formotion/form.rb +173 -0
- data/lib/formotion/form_controller.rb +102 -0
- data/lib/formotion/form_delegate.rb +87 -0
- data/lib/formotion/patch/ui_text_field.rb +158 -0
- data/lib/formotion/row.rb +211 -0
- data/lib/formotion/row_cell_builder.rb +168 -0
- data/lib/formotion/row_type.rb +33 -0
- data/lib/formotion/section.rb +108 -0
- data/lib/formotion/version.rb +3 -0
- data/spec/form_spec.rb +106 -0
- data/spec/row_spec.rb +25 -0
- data/spec/section_spec.rb +20 -0
- metadata +105 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
module Formotion
|
2
|
+
class Row < Formotion::Base
|
3
|
+
PROPERTIES = [
|
4
|
+
# @form.render will contain row's value as the value for this key.
|
5
|
+
:key,
|
6
|
+
# the user's (or configured) value for this row.
|
7
|
+
:value,
|
8
|
+
# set as cell.titleLabel.text
|
9
|
+
:title,
|
10
|
+
# set as cell.detailLabel.text
|
11
|
+
:subtitle,
|
12
|
+
# configures the type of input this is (string, phone, switch, etc)
|
13
|
+
# either Formotion::RowType or a string/symbol representation of one
|
14
|
+
# see row_type.rb
|
15
|
+
:type,
|
16
|
+
|
17
|
+
# The following apply only to text-input fields
|
18
|
+
|
19
|
+
# placeholder text
|
20
|
+
:placeholder,
|
21
|
+
# whether or not the entry field is secure (like a password)
|
22
|
+
:secure,
|
23
|
+
# given by a UIReturnKey___ integer, string, or symbol
|
24
|
+
# EX :default, :google
|
25
|
+
:return_key,
|
26
|
+
# given by a UITextAutocorrectionType___ integer, string, or symbol
|
27
|
+
# EX :yes, :no, :default
|
28
|
+
:auto_correction,
|
29
|
+
# given by a UITextAutocapitalizationType___ integer, string, or symbol
|
30
|
+
# EX :none, :words
|
31
|
+
:auto_capitalization,
|
32
|
+
# field.clearButtonMode; given by a UITextFieldViewMode__ integer, string, symbol
|
33
|
+
# EX :never, :while_editing
|
34
|
+
# DEFAULT is nil, which is used as :while_editing
|
35
|
+
:clear_button]
|
36
|
+
PROPERTIES.each {|prop|
|
37
|
+
attr_accessor prop
|
38
|
+
}
|
39
|
+
BOOLEAN_PROPERTIES = [:secure]
|
40
|
+
SERIALIZE_PROPERTIES = PROPERTIES
|
41
|
+
|
42
|
+
# Reference to the row's section
|
43
|
+
attr_accessor :section
|
44
|
+
|
45
|
+
# Index of the row in the section
|
46
|
+
attr_accessor :index
|
47
|
+
|
48
|
+
# The reuse-identifier used in UITableViews
|
49
|
+
# By default is a stringification of section.index and row.index,
|
50
|
+
# thus is unique per row (bad for memory, to fix later.)
|
51
|
+
attr_accessor :reuse_identifier
|
52
|
+
|
53
|
+
# The following apply only to text-input fields
|
54
|
+
|
55
|
+
# reference to the row's UITextField.
|
56
|
+
attr_reader :text_field
|
57
|
+
|
58
|
+
# callback for what happens when the user
|
59
|
+
# hits the enter key while editing #text_field
|
60
|
+
attr_accessor :on_enter_callback
|
61
|
+
# callback for what happens when the user
|
62
|
+
# starts editing #text_field.
|
63
|
+
attr_accessor :on_begin_callback
|
64
|
+
|
65
|
+
def initialize(params = {})
|
66
|
+
super
|
67
|
+
|
68
|
+
BOOLEAN_PROPERTIES.each {|prop|
|
69
|
+
Formotion::Conditions.assert_nil_or_boolean(self.send(prop))
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Makes all ::BOOLEAN_PROPERTIES queriable with an appended ?
|
74
|
+
# these should be done with alias_method but there's currently a bug
|
75
|
+
# in RM which messes up attr_accessors with alias_method
|
76
|
+
# EX
|
77
|
+
# row.editable?
|
78
|
+
# => true
|
79
|
+
# row.checkable?
|
80
|
+
# => nil
|
81
|
+
def method_missing(method, *args, &block)
|
82
|
+
boolean_method = (method.to_s[0..-2]).to_sym
|
83
|
+
if BOOLEAN_PROPERTIES.member? boolean_method
|
84
|
+
return self.send(boolean_method)
|
85
|
+
end
|
86
|
+
super
|
87
|
+
end
|
88
|
+
|
89
|
+
#########################
|
90
|
+
# pseudo-properties
|
91
|
+
|
92
|
+
def index_path
|
93
|
+
NSIndexPath.indexPathForRow(self.index, inSection:self.section.index)
|
94
|
+
end
|
95
|
+
|
96
|
+
def form
|
97
|
+
self.section.form
|
98
|
+
end
|
99
|
+
|
100
|
+
def reuse_identifier
|
101
|
+
@reuse_identifier || "SECTION_#{self.section.index}_ROW_#{self.index}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def next_row
|
105
|
+
# if there are more rows in this section, use that.
|
106
|
+
return self.section.rows[self.index + 1] if self.index < (self.section.rows.count - 1)
|
107
|
+
|
108
|
+
# if there are more sections, then use the first row of that section.
|
109
|
+
return self.section.next_section.rows[0] if self.section.next_section
|
110
|
+
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def previous_row
|
115
|
+
return self.section.rows[self.index - 1] if self.index > 0
|
116
|
+
|
117
|
+
# if there are more sections, then use the first row of that section.
|
118
|
+
return self.section.previous_section.rows[-1] if self.section.previous_section
|
119
|
+
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def editable?
|
124
|
+
Formotion::RowType::TEXT_FIELD_TYPES.member? self.type
|
125
|
+
end
|
126
|
+
|
127
|
+
def submit_button?
|
128
|
+
self.type == Formotion::RowType::SUBMIT
|
129
|
+
end
|
130
|
+
|
131
|
+
def switchable?
|
132
|
+
self.type == Formotion::RowType::SWITCH
|
133
|
+
end
|
134
|
+
|
135
|
+
def checkable?
|
136
|
+
self.type == Formotion::RowType::CHECK
|
137
|
+
end
|
138
|
+
|
139
|
+
#########################
|
140
|
+
# setter overrides
|
141
|
+
def type=(type)
|
142
|
+
@type = Formotion::RowType.for(type)
|
143
|
+
end
|
144
|
+
|
145
|
+
def return_key=(value)
|
146
|
+
@return_key = const_int_get("UIReturnKey", value)
|
147
|
+
end
|
148
|
+
|
149
|
+
def auto_correction=(value)
|
150
|
+
@auto_correction = const_int_get("UITextAutocorrectionType", value)
|
151
|
+
end
|
152
|
+
|
153
|
+
def auto_capitalization=(value)
|
154
|
+
@auto_capitalization = const_int_get("UITextAutocapitalizationType", value)
|
155
|
+
end
|
156
|
+
|
157
|
+
def clear_button=(value)
|
158
|
+
@clear_button = const_int_get("UITextFieldViewMode", value)
|
159
|
+
end
|
160
|
+
|
161
|
+
#########################
|
162
|
+
# setters for callbacks
|
163
|
+
|
164
|
+
def on_enter(&block)
|
165
|
+
self.on_enter_callback = block
|
166
|
+
end
|
167
|
+
|
168
|
+
def on_begin(&block)
|
169
|
+
self.on_begin_callback = block
|
170
|
+
end
|
171
|
+
|
172
|
+
#########################
|
173
|
+
# Methods for making cells
|
174
|
+
# Called in UITableViewDataSource methods
|
175
|
+
# in form_delegate.rb
|
176
|
+
def make_cell
|
177
|
+
cell, text_field = Formotion::RowCellBuilder.make_cell(self)
|
178
|
+
@text_field = text_field
|
179
|
+
cell
|
180
|
+
end
|
181
|
+
|
182
|
+
#########################
|
183
|
+
# Retreiving data
|
184
|
+
def to_hash
|
185
|
+
super
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
def const_int_get(base, value)
|
190
|
+
return value if value.is_a? Integer
|
191
|
+
value = value.to_s.camelize
|
192
|
+
Kernel.const_get("#{base}#{value}")
|
193
|
+
end
|
194
|
+
|
195
|
+
# Looks like RubyMotion adds UIKit constants
|
196
|
+
# at compile time. If you don't use these
|
197
|
+
# directly in your code, they don't get added
|
198
|
+
# to Kernel and const_int_get crashes.
|
199
|
+
def load_constants_hack
|
200
|
+
[UITextAutocapitalizationTypeNone, UITextAutocapitalizationTypeWords,
|
201
|
+
UITextAutocapitalizationTypeSentences,UITextAutocapitalizationTypeAllCharacters,
|
202
|
+
UITextAutocorrectionTypeNo, UITextAutocorrectionTypeYes, UITextAutocorrectionTypeDefault,
|
203
|
+
UIReturnKeyDefault, UIReturnKeyGo, UIReturnKeyGoogle, UIReturnKeyJoin,
|
204
|
+
UIReturnKeyNext, UIReturnKeyRoute, UIReturnKeySearch, UIReturnKeySend,
|
205
|
+
UIReturnKeyYahoo, UIReturnKeyDone, UIReturnKeyEmergencyCall,
|
206
|
+
UITextFieldViewModeNever, UITextFieldViewModeAlways, UITextFieldViewModeWhileEditing,
|
207
|
+
UITextFieldViewModeUnlessEditing
|
208
|
+
]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
#################
|
2
|
+
#
|
3
|
+
# Formotion::RowCellBuilder
|
4
|
+
# RowCellBuilder handles taking Formotion::Rows
|
5
|
+
# and configuring UITableViewCells based on their properties.
|
6
|
+
#
|
7
|
+
#################
|
8
|
+
module Formotion
|
9
|
+
class RowCellBuilder
|
10
|
+
# The new UITextField in a UITableViewCell
|
11
|
+
# will be assigned this tag, if applicable.
|
12
|
+
TEXT_FIELD_TAG=1000
|
13
|
+
|
14
|
+
# PARAMS row.is_a? Formotion::Row
|
15
|
+
# RETURNS [cell configured to that row, a UITextField for that row if applicable or nil]
|
16
|
+
def self.make_cell(row)
|
17
|
+
cell, text_field = nil
|
18
|
+
|
19
|
+
cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:row.reuse_identifier)
|
20
|
+
|
21
|
+
cell.accessoryType = UITableViewCellAccessoryNone
|
22
|
+
cell.textLabel.text = row.title
|
23
|
+
cell.detailTextLabel.text = row.subtitle
|
24
|
+
|
25
|
+
if row.submit_button?
|
26
|
+
make_submit_cell(row, cell)
|
27
|
+
elsif row.switchable?
|
28
|
+
make_switch_cell(row, cell)
|
29
|
+
elsif row.checkable?
|
30
|
+
make_check_cell(row, cell)
|
31
|
+
elsif row.editable?
|
32
|
+
text_field = make_text_field(row, cell)
|
33
|
+
end
|
34
|
+
[cell, text_field]
|
35
|
+
end
|
36
|
+
|
37
|
+
# This is actually called whenever again cell is checked/unchecked
|
38
|
+
# in the UITableViewDelegate callbacks. So (for now) don't
|
39
|
+
# instantiate long-lived objects in them.
|
40
|
+
# Maybe that logic should be moved elsewhere?
|
41
|
+
def self.make_check_cell(row, cell)
|
42
|
+
cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.make_switch_cell(row, cell)
|
46
|
+
cell.selectionStyle = UITableViewCellSelectionStyleNone
|
47
|
+
switchView = UISwitch.alloc.initWithFrame(CGRectZero)
|
48
|
+
cell.accessoryView = switchView
|
49
|
+
switchView.setOn(row.value || false, animated:false)
|
50
|
+
switchView.when(UIControlEventValueChanged) do
|
51
|
+
row.value = switchView.isOn
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Does a clever little trick to override #layoutSubviews
|
56
|
+
# for just this one UITableViewCell object, in order to
|
57
|
+
# center it's labels horizontally.
|
58
|
+
def self.make_submit_cell(row, cell)
|
59
|
+
cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews)
|
60
|
+
cell.instance_eval do
|
61
|
+
def layoutSubviews
|
62
|
+
old_layoutSubviews
|
63
|
+
|
64
|
+
center = lambda {|frame, dimen|
|
65
|
+
((self.frame.size.send(dimen) - frame.size.send(dimen)) / 2.0)
|
66
|
+
}
|
67
|
+
|
68
|
+
self.textLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.textLabel.center.y)
|
69
|
+
self.detailTextLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.detailTextLabel.center.y)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Configures the cell to have a new UITextField
|
75
|
+
# which is used to enter data. Consists of
|
76
|
+
# 1) setting up that field with the appropriate properties
|
77
|
+
# specified by `row` 2) configures the callbacks on the field
|
78
|
+
# to call any callbacks `row` listens for.
|
79
|
+
# Also does the layoutSubviews swizzle trick
|
80
|
+
# to size the UITextField so it won't bump into the titleLabel.
|
81
|
+
def self.make_text_field(row, cell)
|
82
|
+
field = UITextField.alloc.initWithFrame(CGRectZero)
|
83
|
+
field.tag = TEXT_FIELD_TAG
|
84
|
+
|
85
|
+
field.placeholder = row.placeholder
|
86
|
+
field.text = row.value
|
87
|
+
|
88
|
+
field.clearButtonMode = UITextFieldViewModeWhileEditing
|
89
|
+
field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter
|
90
|
+
field.textAlignment = UITextAlignmentRight
|
91
|
+
|
92
|
+
case row.type
|
93
|
+
when RowType::EMAIL
|
94
|
+
field.keyboardType = UIKeyboardTypeEmailAddress
|
95
|
+
when RowType::PHONE
|
96
|
+
field.keyboardType = UIKeyboardTypePhonePad
|
97
|
+
when RowType::NUMBER
|
98
|
+
field.keyboardType = UIKeyboardTypeDecimalPad
|
99
|
+
else
|
100
|
+
field.keyboardType = UIKeyboardTypeDefault
|
101
|
+
end
|
102
|
+
|
103
|
+
field.secureTextEntry = true if row.secure?
|
104
|
+
field.returnKeyType = row.return_key || UIReturnKeyNext
|
105
|
+
field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization
|
106
|
+
field.autocorrectionType = row.auto_correction if row.auto_correction
|
107
|
+
field.clearButtonMode = row.clear_button || UITextFieldViewModeWhileEditing
|
108
|
+
|
109
|
+
if row.on_enter_callback
|
110
|
+
field.should_return? do |text_field|
|
111
|
+
if row.on_enter_callback.arity == 0
|
112
|
+
row.on_enter_callback.call
|
113
|
+
elsif row.on_enter_callback.arity == 1
|
114
|
+
row.on_enter_callback.call(row)
|
115
|
+
end
|
116
|
+
false
|
117
|
+
end
|
118
|
+
elsif field.returnKeyType == UIReturnKeyDone
|
119
|
+
field.should_return? do |text_field|
|
120
|
+
text_field.resignFirstResponder
|
121
|
+
false
|
122
|
+
end
|
123
|
+
else
|
124
|
+
field.should_return? do |text_field|
|
125
|
+
if row.next_row && row.next_row.text_field
|
126
|
+
row.next_row.text_field.becomeFirstResponder
|
127
|
+
else
|
128
|
+
text_field.resignFirstResponder
|
129
|
+
end
|
130
|
+
true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
field.on_begin do |text_field|
|
135
|
+
row.on_begin_callback && row.on_begin_callback.call
|
136
|
+
end
|
137
|
+
|
138
|
+
field.should_begin? do |text_field|
|
139
|
+
row.section.form.active_row = row
|
140
|
+
true
|
141
|
+
end
|
142
|
+
|
143
|
+
field.on_change do |text_field|
|
144
|
+
row.value = text_field.text
|
145
|
+
end
|
146
|
+
|
147
|
+
cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews)
|
148
|
+
cell.instance_eval do
|
149
|
+
def layoutSubviews
|
150
|
+
old_layoutSubviews
|
151
|
+
|
152
|
+
# viewWithTag is terrible, but I think it's ok to use here...
|
153
|
+
formotion_field = self.viewWithTag(Formotion::RowCellBuilder::TEXT_FIELD_TAG)
|
154
|
+
formotion_field.sizeToFit
|
155
|
+
|
156
|
+
field_frame = formotion_field.frame
|
157
|
+
field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
|
158
|
+
field_frame.origin.y = ((self.frame.size.height - field_frame.size.height) / 2.0).round
|
159
|
+
field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
|
160
|
+
formotion_field.frame = field_frame
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
cell.addSubview(field)
|
165
|
+
field
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Formotion
|
2
|
+
class RowType
|
3
|
+
STRING=0
|
4
|
+
EMAIL=1
|
5
|
+
PHONE=2
|
6
|
+
NUMBER=3
|
7
|
+
SUBMIT=4
|
8
|
+
SWITCH=5
|
9
|
+
CHECK=6
|
10
|
+
STATIC=100
|
11
|
+
|
12
|
+
TYPES = [STRING, EMAIL, PHONE, NUMBER, SUBMIT, SWITCH, CHECK, STATIC]
|
13
|
+
TEXT_FIELD_TYPES=[STRING, EMAIL, PHONE, NUMBER]
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def for(string_or_sym_or_int)
|
17
|
+
type = string_or_sym_or_int
|
18
|
+
|
19
|
+
if type.is_a?(Symbol) or type.is_a? String
|
20
|
+
string = type.to_s.upcase
|
21
|
+
if not const_defined? string
|
22
|
+
raise Formotion::InvalidClassError, "Invalid RowType value #{string_or_sym}"
|
23
|
+
end
|
24
|
+
Formotion::RowType.const_get(string)
|
25
|
+
elsif type.is_a? Integer and TYPES.member? type
|
26
|
+
TYPES[type]
|
27
|
+
else
|
28
|
+
raise Formotion::InvalidClassError, "Attempted row type #{type.inspect} is not a valid RowType."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Formotion
|
2
|
+
class Section < Formotion::Base
|
3
|
+
PROPERTIES = [
|
4
|
+
# Displayed in the section header row
|
5
|
+
:title,
|
6
|
+
# Arranges the section as a 'radio' section,
|
7
|
+
# such that only one row can be checked at a time.
|
8
|
+
:select_one,
|
9
|
+
# IF :select_one is true, then @form.render will contain
|
10
|
+
# the checked row's value as the value for this key.
|
11
|
+
# ELSE it does nothing.
|
12
|
+
:key
|
13
|
+
]
|
14
|
+
PROPERTIES.each {|prop|
|
15
|
+
attr_accessor prop
|
16
|
+
}
|
17
|
+
SERIALIZE_PROPERTIES = PROPERTIES + [:rows]
|
18
|
+
|
19
|
+
# Relationships
|
20
|
+
|
21
|
+
# This section's form
|
22
|
+
attr_accessor :form
|
23
|
+
|
24
|
+
# This section's index in it's form.
|
25
|
+
attr_accessor :index
|
26
|
+
|
27
|
+
def initialize(params = {})
|
28
|
+
super
|
29
|
+
|
30
|
+
Formotion::Conditions.assert_nil_or_boolean(self.select_one)
|
31
|
+
|
32
|
+
rows = params[:rows] || params["rows"]
|
33
|
+
rows && rows.each {|row_hash|
|
34
|
+
row = create_row(row_hash)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_row(&block)
|
39
|
+
row = create_row
|
40
|
+
block.call(row)
|
41
|
+
row
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_row(hash = {})
|
45
|
+
row = hash
|
46
|
+
if hash.class == Hash
|
47
|
+
row = Formotion::Row.new(hash)
|
48
|
+
end
|
49
|
+
row.section = self
|
50
|
+
row.index = self.rows.count
|
51
|
+
self.rows << row
|
52
|
+
row
|
53
|
+
end
|
54
|
+
|
55
|
+
#########################
|
56
|
+
# attribute overrides
|
57
|
+
|
58
|
+
def rows
|
59
|
+
@rows ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
def rows=(rows)
|
63
|
+
rows.each {|row|
|
64
|
+
Formotion::Conditions.assert_class(row, Formotion::Row)
|
65
|
+
}
|
66
|
+
@rows = rows
|
67
|
+
end
|
68
|
+
|
69
|
+
def index=(index)
|
70
|
+
@index = index.to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
#########################
|
74
|
+
# pseudo-properties
|
75
|
+
|
76
|
+
# should be done with alias_method but there's currently a bug
|
77
|
+
# in RM which messes up attr_accessors with alias_method
|
78
|
+
def select_one?
|
79
|
+
self.select_one
|
80
|
+
end
|
81
|
+
|
82
|
+
def next_section
|
83
|
+
# if there are more sections in this form, use that.
|
84
|
+
if self.index < self.form.sections.count - 1
|
85
|
+
return self.form.sections[self.index + 1]
|
86
|
+
end
|
87
|
+
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def previous_section
|
92
|
+
# if there are more sections in this form, use that.
|
93
|
+
if self.index > 0
|
94
|
+
return self.form.sections[self.index - 1]
|
95
|
+
end
|
96
|
+
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
#########################
|
101
|
+
# Retreiving data
|
102
|
+
def to_hash
|
103
|
+
h = super
|
104
|
+
h[:rows] = self.rows.collect {|row| row.to_hash}
|
105
|
+
h
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|