forma 0.0.0 → 0.1.0

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