forma 0.0.0 → 0.1.0

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/README.md CHANGED
@@ -1,3 +1,43 @@
1
1
  # Forma
2
2
 
3
3
  [![Build Status](https://travis-ci.org/dimakura/forma.png?branch=master)](https://travis-ci.org/dimakura/forma)
4
+
5
+ `Forma` is usefull to easily create rich web forms with ruby web-frameworks.
6
+
7
+ Standard rails forms are great.
8
+ There are also nice libraries for creating some common elements, like SimpleForms.
9
+
10
+ `Forma` is intended for projects with huge amount of forms.
11
+ It scales easily and without a pain.
12
+
13
+ ## Instalation and usage
14
+
15
+ Include
16
+
17
+ gem 'forma'
18
+
19
+ into your Gemfile. Or use
20
+
21
+ gem install forma
22
+
23
+ For proper functionality you should also include `jquery` (v > 1.9).
24
+
25
+ TODO: css & js inclusion ... etc.
26
+
27
+ ## Examples
28
+
29
+ Below are some examples with `forma`:
30
+
31
+ ```ruby
32
+ forma_for @user, title: 'Register', url: register_path do |f|
33
+ f.text_field :username
34
+ f.password_field :password
35
+ f.password_field :password_confirmation
36
+ f.text_field :first_name
37
+ f.text_field :last_name
38
+ f.text_field :mobile
39
+ f.web_field :email
40
+ end
41
+ ```
42
+
43
+ TODO: more advanced usage of forms
data/Rakefile CHANGED
@@ -18,4 +18,12 @@ end
18
18
 
19
19
  task :less do
20
20
  less_to_css('forma')
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
21
29
  end
@@ -0,0 +1,45 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Forma
3
+ class Action
4
+ include Forma::Html
5
+ # attr_reader :label, :icon, :method, :confirm, :as
6
+ attr_reader :url
7
+
8
+ def initialize(h={})
9
+ h = h.symbolize_keys
10
+ @id = h[:id]
11
+ @label = h[:label]
12
+ @icon = h[:icon]
13
+ @url = h[:url]
14
+ @method = h[:method]
15
+ @confirm = h[:confirm]
16
+ @as = h[:as]
17
+ @tooltip = h[:tooltip]
18
+ end
19
+
20
+ def to_html(model)
21
+ children = [ (el('img', attrs: { src: @icon }) if @icon.present?), el('span', text: eval_label(model)) ]
22
+ button = (@as.to_s == 'button')
23
+ el(
24
+ 'a',
25
+ attrs: {
26
+ id: @id,
27
+ class: ['ff-action', ('btn' if button)],
28
+ href: eval_url(model), 'data-method' => @method, 'data-confirm' => @confirm,
29
+ 'data-original-title' => @tooltip
30
+ },
31
+ children: children
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def eval_url(model)
38
+ @url.is_a?(Proc) ? @url.call(model) : @url.to_s
39
+ end
40
+
41
+ def eval_label(model)
42
+ @label.is_a?(Proc) ? @label.call(model) : @label.to_s
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,68 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'singleton'
3
+
4
+ module Forma
5
+
6
+ class << self
7
+ def config
8
+ Forma::Config.instance
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ class Config
15
+ include Singleton
16
+
17
+ def num
18
+ @num ||= NumberConfig.new
19
+ end
20
+
21
+ def date
22
+ @date ||= DateConfig.new
23
+ end
24
+
25
+ def texts
26
+ @texts ||= TextsConfig.new
27
+ end
28
+
29
+ def map
30
+ @map ||= MapConfig.new
31
+ end
32
+ end
33
+
34
+ class NumberConfig
35
+ attr_accessor :delimiter
36
+ attr_accessor :separator
37
+ def initialize
38
+ self.delimiter = ','
39
+ self.separator = '.'
40
+ end
41
+ end
42
+
43
+ class DateConfig
44
+ attr_accessor :formatter
45
+ def initialize
46
+ self.formatter = '%d-%b-%Y'
47
+ end
48
+ end
49
+
50
+ class TextsConfig
51
+ attr_accessor :empty, :table_empty
52
+ def initialize
53
+ self.empty = '(empty)'
54
+ self.table_empty = '(no data)'
55
+ end
56
+ end
57
+
58
+ class MapConfig
59
+ attr_accessor :default_latitude
60
+ attr_accessor :default_longitude
61
+ attr_accessor :zoom_level
62
+ def initialize
63
+ self.default_latitude = 41.711447
64
+ self.default_longitude = 44.754514
65
+ self.zoom_level = 17
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,451 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Forma
3
+ # General field interface.
4
+ class Field
5
+ include Forma::Utils
6
+ include Forma::Html
7
+
8
+ attr_reader :label, :hint, :i18n, :name, :tag
9
+ attr_reader :required, :autofocus, :readonly
10
+ attr_reader :width, :height
11
+ attr_reader :before, :after
12
+ attr_reader :url, :icon
13
+ attr_accessor :model, :value, :parent, :child_model_name
14
+ attr_writer :model_name
15
+ attr_reader :actions
16
+
17
+ def initialize(h = {})
18
+ h = h.symbolize_keys
19
+ @id = h[:id]; @label = h[:label]; @hint = h[:hint]; @i18n = h[:i18n]
20
+ @required = h[:required]; @autofocus = h[:autofocus]; @readonly = (not not h[:readonly])
21
+ @width = h[:width]; @height = h[:height]
22
+ @before = h[:before]; @after = h[:after]
23
+ @name = h[:name]; @value = h[:value]
24
+ @url = h[:url]; @icon = h[:icon]
25
+ @model = h[:model]; @parent = h[:parent]
26
+ @model_name = h[:model_name]; @child_model_name = h[:child_model_name]
27
+ @actions = h[:actions] || []
28
+ @tag = h[:tag]
29
+ end
30
+
31
+ def action(url, h={})
32
+ h[:url] = url
33
+ @actions << Action.new(h)
34
+ end
35
+
36
+ def name_as_chain
37
+ if self.parent and self.parent.respond_to?(:name_as_chain)
38
+ chain = self.parent.name_as_chain
39
+ chain << self.name
40
+ else
41
+ chain = [ self.model_name, self.name ]
42
+ end
43
+ end
44
+
45
+ def id
46
+ if @id then @id
47
+ else name_as_chain.flatten.join('_') end
48
+ end
49
+
50
+ def parameter_name
51
+ chain = name_as_chain; length = chain.length
52
+ p_name = ''
53
+ chain.reverse.each_with_index do |n, i|
54
+ if i == 0 then p_name = n
55
+ elsif i == length - 1 then p_name = "#{n}[#{p_name}]"
56
+ else p_name = "#{n}_attributes[#{p_name}]" end
57
+ end
58
+ p_name
59
+ end
60
+
61
+ # Convert this element into HTML.
62
+ def to_html(edit)
63
+ val = self.value
64
+ if edit and not readonly
65
+ edit = edit_element(val)
66
+ el('div', children: [ before_element, icon_element, edit, after_element, actions_element ])
67
+ else
68
+ if val.present? or val == false
69
+ view = view_element(val)
70
+ view = el('a', attrs: { href: eval_url }, children: [ view ]) if @url
71
+ el('div', children: [ before_element, icon_element, view, after_element, actions_element ])
72
+ else
73
+ empty = empty_element
74
+ el('div', children: [ empty, actions_element ])
75
+ end
76
+ end
77
+ end
78
+
79
+ # Returns model name.
80
+ # Model name can be defined by user or determined automatically, based on model class.
81
+ def model_name
82
+ if @model_name then @model_name
83
+ elsif @parent then @parent.child_model_name
84
+ else singular_name(self.model)
85
+ end
86
+ end
87
+
88
+ def localization_key
89
+ if @i18n.present?
90
+ ["models", self.model_name, @i18n].compact.join('.')
91
+ elsif self.respond_to?(:name)
92
+ ["models", self.model_name, self.name].compact.join('.')
93
+ end
94
+ end
95
+
96
+ def localized_label
97
+ self.label.present? ? self.label : I18n.t(localization_key, default: self.name)
98
+ end
99
+
100
+ def localized_hint
101
+ self.hint.present? ? self.hint : I18n.t("#{localization_key}_hint", default: '')
102
+ end
103
+
104
+ protected
105
+
106
+ def icon_element
107
+ el('img', attrs: { src: eval_icon, style: { 'margin-right' => '4px' } }) if @icon.present?
108
+ end
109
+
110
+ def before_element
111
+ el('span', text: before, attrs: { class: 'ff-field-before' }) if before.present?
112
+ end
113
+
114
+ def after_element
115
+ el('span', text: after, attrs: { class: 'ff-field-after' }) if after.present?
116
+ end
117
+
118
+ def empty_element
119
+ el('span', attrs: { class: 'ff-empty' }, text: Forma.config.texts.empty)
120
+ end
121
+
122
+ def actions_element
123
+ if @actions.any?
124
+ el('div', attrs: { class: 'ff-field-actions' }, children: @actions.map { |action| action.to_html(@model) })
125
+ end
126
+ end
127
+
128
+ def eval_url
129
+ @url.is_a?(Proc) ? @url.call(self.model) : @url.to_s
130
+ end
131
+
132
+ def eval_icon
133
+ @icon.is_a?(Proc) ? @icon.call(self.model) : @icon.to_s
134
+ end
135
+ end
136
+
137
+ # Complex field.
138
+ class ComplexField < Field
139
+ include Forma::FieldHelper
140
+ attr_reader :fields
141
+
142
+ def initialize(h = {})
143
+ h = h.symbolize_keys
144
+ @fields = h[:fields] || []
145
+ super(h)
146
+ end
147
+
148
+ def add_field(f)
149
+ @fields << f
150
+ end
151
+
152
+ def value
153
+ val = super
154
+ if val then val
155
+ else
156
+ @fields.each { |f| f.model = self.model }
157
+ @fields.map { |f| f.value }
158
+ end
159
+ end
160
+
161
+ def edit_element(val)
162
+ el(
163
+ 'div',
164
+ attrs: { class: 'ff-complex-field' },
165
+ children: @fields.zip(val).map { |fv|
166
+ el(
167
+ 'div',
168
+ attrs: { class: 'ff-field' },
169
+ children: [ fv[0].edit_element(fv[1]) ]
170
+ )
171
+ }
172
+ )
173
+ end
174
+
175
+ def view_element(val)
176
+ el(
177
+ 'div',
178
+ attrs: { class: 'ff-complex-field' },
179
+ children: @fields.zip(val).map { |fv|
180
+ el(
181
+ 'div',
182
+ attrs: { class: 'ff-complex-part' },
183
+ children: [ fv[0].view_element(fv[1]) ]
184
+ )
185
+ }
186
+ )
187
+ end
188
+ end
189
+
190
+ # Map field.
191
+ class MapField < Field
192
+ def value
193
+ val = super
194
+ if val then val
195
+ else
196
+ lat = extract_value(self.model, "#{self.name}_latitude") || Forma.config.map.default_latitude
197
+ long = extract_value(self.model, "#{self.name}_longitude") || Forma.config.map.default_longitude
198
+ [ lat, long ]
199
+ end
200
+ end
201
+
202
+ def width; @width || 500 end
203
+ def height; @height || 500 end
204
+
205
+ def view_element(val)
206
+ el('div', attrs: { style: { width: "#{self.width}px", height: "#{self.height}px", position: 'relative' } }, children: [
207
+ el('div', attrs: { id: self.id, class: 'ff-map' }),
208
+ # google_import,
209
+ map_display(val, false)
210
+ ])
211
+ end
212
+
213
+ def edit_element(val)
214
+ el('div', attrs: { style: { width: "#{self.width}px", height: "#{self.height}px", position: 'relative' } }, children: [
215
+ el('div', attrs: { id: self.id, class: 'ff-map' }),
216
+ # google_import,
217
+ map_display(val, true),
218
+ el('input', attrs: { name: latitude_name, id: "#{self.id}_latitude", value: val[0], type: 'hidden' }),
219
+ el('input', attrs: { name: longitude_name, id: "#{self.id}_longitude", value: val[1], type: 'hidden' }),
220
+ ])
221
+ end
222
+
223
+ private
224
+
225
+ def map_display(val, edit)
226
+ longLat = "{ latitude: #{val[0]}, longitude: #{val[1]} }"
227
+ zoom_level = Forma.config.map.zoom_level
228
+ el(
229
+ 'script',
230
+ attrs: { type: 'text/javascript' },
231
+ html: %Q{ forma.registerGoogleMap('#{self.id}', #{zoom_level}, #{longLat}, [ #{longLat} ], #{edit}); }
232
+ )
233
+ end
234
+
235
+ def latitude_name
236
+ "#{name_as_chain[0]}[#{name_as_chain[1]}_latitude]"
237
+ end
238
+
239
+ def longitude_name
240
+ "#{name_as_chain[0]}[#{name_as_chain[1]}_longitude]"
241
+ end
242
+ end
243
+
244
+ # SimpleField gets it's value from it's name.
245
+ class SimpleField < Field
246
+ def initialize(h = {})
247
+ h = h.symbolize_keys
248
+ super(h)
249
+ end
250
+
251
+ def value
252
+ val = super
253
+ if val then val
254
+ else extract_value(self.model, self.name)
255
+ end
256
+ end
257
+
258
+ def errors
259
+ if self.model.respond_to?(:errors); self.model.errors.messages[name.to_sym] end || []
260
+ end
261
+
262
+ def has_errors?
263
+ errors.any?
264
+ end
265
+ end
266
+
267
+ # Subform!
268
+ class SubformField < SimpleField
269
+ include Forma::FieldHelper
270
+ attr_reader :form
271
+
272
+ def initialize(h = {})
273
+ h[:label] = false
274
+ @form = Form.new(collapsible: true)
275
+ super(h)
276
+ end
277
+
278
+ def edit_element(val)
279
+ init_forma_before_field_display(true)
280
+ @form.to_html
281
+ end
282
+
283
+ def view_element(val)
284
+ init_forma_before_field_display(false)
285
+ @form.to_html
286
+ end
287
+
288
+ private
289
+
290
+ def init_forma_before_field_display(edit)
291
+ @form.model = val
292
+ @form.parent_field = self
293
+ @form.edit = edit
294
+ @form.icon = eval_icon if @icon
295
+ @form.title = localized_label
296
+ end
297
+ end
298
+
299
+ # Text field.
300
+ class TextField < SimpleField
301
+ attr_reader :password
302
+
303
+ def initialize(h = {})
304
+ h = h.symbolize_keys
305
+ @password = h[:password]
306
+ super(h)
307
+ end
308
+
309
+ def view_element(val)
310
+ el((@tag || 'span'), text: (password ? '******' : val.to_s), attrs: { id: self.id })
311
+ end
312
+
313
+ def edit_element(val)
314
+ el('input', attrs: {
315
+ id: self.id,
316
+ name: parameter_name,
317
+ type: (password ? 'password' : 'text'),
318
+ value: val.to_s,
319
+ autofocus: @autofocus,
320
+ style: { width: ("#{width}px" if width.present?) }
321
+ })
322
+ end
323
+ end
324
+
325
+ # Email field.
326
+ class EmailField < TextField
327
+ def view_element(val)
328
+ el('a', attrs: { href: "mailto:#{val}" }, text: val)
329
+ end
330
+ end
331
+
332
+ # Date feild.
333
+ class DateField < SimpleField
334
+ attr_reader :formatter
335
+
336
+ def initialize(h = {})
337
+ h = h.symbolize_keys
338
+ @formatter = h[:formatter]
339
+ super(h)
340
+ end
341
+
342
+ def view_element(val)
343
+ el('span', text: val.localtime.strftime(formatter || Forma.config.date.formatter))
344
+ end
345
+
346
+ def edit_element(val)
347
+ el('input', attrs: {
348
+ name: parameter_name,
349
+ type: 'text',
350
+ value: val.to_s,
351
+ autofocus: @autofocus,
352
+ style: { width: ("#{width}px" if width.present?) }
353
+ })
354
+ end
355
+ end
356
+
357
+ # Boolean field.
358
+ class BooleanField < SimpleField
359
+ def view_element(val)
360
+ el('input', attrs: { type: 'checkbox', disabled: true, checked: ('checked' if val) })
361
+ end
362
+
363
+ def edit_element(val)
364
+ e1 = el('input', attrs: { type: 'hidden', name: parameter_name, value: "0"})
365
+ e2 = el('input', attrs: { type: 'checkbox', name: parameter_name, checked: ('checked' if val), value: "1"})
366
+ el('span', children: [ e1, e2 ])
367
+ end
368
+ end
369
+
370
+ # Image upload field.
371
+ class ImageField < SimpleField
372
+ def view_element(val)
373
+ el('img', attrs: { src: val.url } )
374
+ end
375
+
376
+ def edit_element(val)
377
+ el('input', attrs: {
378
+ name: parameter_name,
379
+ type: 'file',
380
+ })
381
+ end
382
+ end
383
+
384
+ # Number field.
385
+ class NumberField < TextField
386
+ def view_element(val)
387
+ el('code', text: "#{val}")
388
+ end
389
+ end
390
+
391
+ # Combo field.
392
+ class ComboField < SimpleField
393
+ def initialize(h={})
394
+ h = h.symbolize_keys
395
+ @empty = h[:empty]
396
+ @default = h[:default]
397
+ @collection = (h[:collection] || [])
398
+ super(h)
399
+ end
400
+
401
+ def view_element(val)
402
+ data = normalize_data(@collection, false)
403
+ text = data.find{|text, value| val == value }[0].to_s rescue nil
404
+ el('span', text: text)
405
+ end
406
+
407
+ def edit_element(val)
408
+ data = normalize_data(@collection, @empty)
409
+ selection = val.present? ? val : @default
410
+ el('select', attrs: { name: parameter_name }, children: data.map { |text, value|
411
+ if value.nil? then el('option', attrs: { selected: selection.blank? }, text: text)
412
+ else el('option', attrs: { selected: (true if selection == value), value: value }, text: text)
413
+ end
414
+ })
415
+ end
416
+
417
+ private
418
+
419
+ def normalize_data(collection, empty)
420
+ if collection.is_a?(Hash) then data = collection.to_a
421
+ else data = collection.map { |x| [x.to_s, x.id] } end
422
+ if empty != false then data.insert[empty.to_s, nil] end
423
+ Hash[data]
424
+ end
425
+ end
426
+
427
+ # Selection field.
428
+ class SelectField < SimpleField
429
+ def initialize(h={})
430
+ h = h.symbolize_keys
431
+ @search_url = h[:search_url]
432
+ @search_width = h[:search_width] || 500
433
+ @search_height = h[:search_height] || 600
434
+ super(h)
435
+ end
436
+
437
+ def view_element(val)
438
+ el(@tag || 'span', text: val.to_s)
439
+ end
440
+
441
+ def edit_element(val)
442
+ el('div', attrs: { id: self.id, class: 'ff-select-field' }, children: [
443
+ el('input', attrs: { id: "#{self.id}_value", type: 'text', value: "#{val and val.id}" }),
444
+ el('span', attrs: { id: "#{self.id}_text" }, text: val.to_s),
445
+ el('a', attrs: { class: 'ff-select-link btn btn-mini', 'data-id' => self.id, 'data-url' => @search_url, 'data-width' => @search_width, 'data-height' => @search_height }, children: [
446
+ el('i', attrs: { class: 'icon icon-search' })
447
+ ])
448
+ ])
449
+ end
450
+ end
451
+ end