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/lib/forma/form.rb ADDED
@@ -0,0 +1,257 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Forma
3
+ # Form.
4
+ class Form
5
+ include Forma::Html
6
+ include Forma::FieldHelper
7
+ include Forma::WithTitleElement
8
+ include Forma::Utils
9
+ attr_accessor :collapsible, :collapsed
10
+ attr_accessor :icon, :title, :title_actions
11
+ attr_accessor :model, :parent_field, :edit
12
+ attr_accessor :model_name
13
+
14
+ def initialize(h = {})
15
+ h = h.symbolize_keys
16
+ # general
17
+ @id = h[:id]
18
+ # @theme = h[:theme] || 'blue'
19
+ # title properties
20
+ @title = h[:title]
21
+ @icon = h[:icon]
22
+ @collapsible = h[:collapsible]
23
+ @collapsed = h[:collapsed]
24
+ # submit options
25
+ @url = h[:url]
26
+ @submit = h[:submit] || 'Save'
27
+ @wait_on_submit = h[:wait_on_submit].blank? ? true : (not not h[:wait_on_submit])
28
+ @method = h[:method]
29
+ @auth_token = h[:auth_token]
30
+ # tabs
31
+ @tabs = h[:tabs] || []
32
+ @selected_tab = h[:selected_tab] || 0
33
+ # model, errors and editing options
34
+ @model = h[:model]
35
+ @errors = h[:errors]
36
+ @edit = h[:edit]
37
+ @model_name = h[:model_name]
38
+ # actions
39
+ @title_actions = h[:title_actions] || []
40
+ @bottom_actions = h[:bottom_actions] || []
41
+ @multipart = (not not h[:multipart])
42
+ end
43
+
44
+ def to_html
45
+ el(
46
+ 'div',
47
+ attrs: { id: @id, class: [ 'ff-form' ] },
48
+ children: [
49
+ title_element,
50
+ body_element,
51
+ ]
52
+ )
53
+ end
54
+
55
+ def submit(txt = nil)
56
+ @submit = txt if txt.present?
57
+ @submit
58
+ end
59
+
60
+ def title_action(url, h={})
61
+ h[:url] = url
62
+ @title_actions << Action.new(h)
63
+ end
64
+
65
+ def bottom_action(url, h={})
66
+ h[:url] = url
67
+ h[:as] = 'button' unless h[:as].present?
68
+ @bottom_actions << Action.new(h)
69
+ end
70
+
71
+ # Adds a new tab and ibject body content.
72
+ def tab(opts={})
73
+ tab = Tab.new(opts)
74
+ @tabs << tab
75
+ yield tab if block_given?
76
+ tab
77
+ end
78
+
79
+ # Adding a new field to this form.
80
+ def add_field(f)
81
+ @tabs = [ Tab.new ] if @tabs.empty?
82
+ @tabs[0].col1.add_field(f)
83
+ end
84
+
85
+ private
86
+
87
+ def body_element
88
+ el(
89
+ (@edit and parent_field.nil?) ? 'form' : 'div',
90
+ attrs: {
91
+ enctype: ('multipart/form-data' if @multipart),
92
+ class: (@wait_on_submit ? ['ff-form-body', 'ff-wait-on-submit', 'ff-collapsible-body'] : ['ff-form-body', 'ff-collapsible-body']),
93
+ action: (@url if @edit), method: (@method if @edit),
94
+ style: ({display: 'none'} if @collapsible && @collapsed)
95
+ },
96
+ children: [
97
+ (errors_element if @errors.present?),
98
+ (auth_token_element if (@edit == true and parent_field.nil?)),
99
+ tabs_element,
100
+ bottom_actions,
101
+ ]
102
+ )
103
+ end
104
+
105
+ def errors_element
106
+ many = @errors.is_a?(Array)
107
+ children = (many ? @errors.map { |e| el('li', text: e.to_s) } : [ el('span', text: @errors.to_s) ])
108
+ el((many ? 'ul' : 'div'), attrs: { class: 'ff-form-errors' }, children: children)
109
+ end
110
+
111
+ def auth_token_element
112
+ if @auth_token.present?
113
+ el('div', attrs: { style: {padding: 0, margin: 0, height: 0, width: 0, display: 'inline'} }, children: [
114
+ el('input', attrs: { type: 'hidden', name: 'authenticity_token', value: @auth_token })
115
+ ])
116
+ end
117
+ end
118
+
119
+ def field_element(fld)
120
+ def field_error_element(errors)
121
+ many = errors.length > 1
122
+ children = (many ? errors.map { |e| el('li', text: e.to_s) } : [el('div', text: errors[0].to_s)])
123
+ el('div', attrs: { class: 'ff-field-errors' }, children: children)
124
+ end
125
+ # X---- field initialization ----X
126
+ fld.model = @model
127
+ fld.parent = self.parent_field
128
+ fld.model_name = self.model_name
129
+ # X------------------------------X
130
+ has_errors = (@edit and @model.present? and fld.respond_to?(:has_errors?) and fld.has_errors?)
131
+ if fld.label != false
132
+ label_text = fld.localized_label
133
+ label_hint = fld.localized_hint
134
+ label_element = el('div', attrs: { class: (fld.required ? ['ff-label', 'ff-required'] : ['ff-label'])},
135
+ text: label_text,
136
+ children: [
137
+ (el('i', attrs: { class: 'ff-field-hint', 'data-toggle' => 'tooltip', title: label_hint }) if label_hint.present?)
138
+ ]
139
+ )
140
+ end
141
+ value_element = el('div', attrs: { class: (fld.required ? ['ff-value', 'ff-required'] : ['ff-value']) }, children: [
142
+ fld.to_html(@edit), (field_error_element(fld.errors) if has_errors),
143
+ ])
144
+ el(
145
+ 'div', attrs: {
146
+ id: ("fld_#{fld.id}" if fld.id), class: (has_errors ? ['ff-field', 'ff-error'] : ['ff-field']) },
147
+ children: [ label_element, value_element ]
148
+ )
149
+ end
150
+
151
+ def tabs_element
152
+ def column_element(col, hasSecondCol)
153
+ if col
154
+ el('div', attrs: { class: ['ff-col', (hasSecondCol ? 'ff-col-50' : 'ff-col-100')]}, children: [
155
+ el('div', attrs: { class: 'ff-col-inner' }, children: col.fields.map { |fld| field_element(fld) })
156
+ ])
157
+ end
158
+ end
159
+ def tab_actions(tab)
160
+ if tab.actions.any?
161
+ el('div', attrs: { class: 'ff-tab-actions' }, children: tab.actions.map { |a| a.to_html(@model) })
162
+ end
163
+ end
164
+ def tab_element(tab)
165
+ hasSecondCol = (not tab.col2.fields.empty?)
166
+ col1 = column_element(tab.col1, hasSecondCol)
167
+ col2 = column_element(tab.col2, hasSecondCol)
168
+ el('div', attrs: { class: 'ff-tab-content',style: ({ display: 'none' } if @tabs.index(tab) != @selected_tab) },
169
+ children: [
170
+ tab_actions(tab),
171
+ el('div', attrs: { class: 'ff-cols'}, children: [col1, col2])
172
+ ]
173
+ )
174
+ end
175
+ def tabs_header
176
+ if @tabs.length > 1
177
+ el('ul', attrs: { class: 'ff-tabs-header' }, children: @tabs.map { |tab|
178
+ el('li', attrs: { class: ('ff-selected' if @tabs.index(tab) == @selected_tab) }, children: [
179
+ (el('img', attrs: { src: tab.icon }) if tab.icon),
180
+ (el('span', text: tab.title || 'No Title'))
181
+ ])
182
+ })
183
+ end
184
+ end
185
+ el(
186
+ 'div',
187
+ attrs: { class: 'ff-tabs' },
188
+ children: [
189
+ tabs_header,
190
+ el('div', attrs: { class: 'ff-tabs-body'}, children: @tabs.map { |tab| tab_element(tab) })
191
+ ]
192
+ )
193
+ end
194
+
195
+ def bottom_actions
196
+ edit = (@edit and parent_field.nil?)
197
+ if edit || @bottom_actions.any?
198
+ children = @bottom_actions.map { |a| a.to_html(@model) }
199
+ save_action = el('button', attrs: { type: 'submit', class: 'btn btn-primary' }, text: @submit) if edit
200
+ el('div', attrs: { class: 'ff-bottom-actions' }, children: [save_action] + children )
201
+ end
202
+ end
203
+ end
204
+
205
+ # This is a tab.
206
+ class Tab
207
+ include Forma::FieldHelper
208
+ attr_reader :title, :icon, :actions
209
+
210
+ def initialize(h = {})
211
+ h = h.symbolize_keys
212
+ @title = h[:title]
213
+ @icon = h[:icon]
214
+ @col1 = h[:col1]
215
+ @col2 = h[:col2]
216
+ @actions = h[:actions] || []
217
+ end
218
+
219
+ # Adding field to this tab.
220
+ def add_field(f)
221
+ col1.add_field(f)
222
+ end
223
+
224
+ # Returns the first column of this tab.
225
+ def col1
226
+ @col1 = Col.new if @col1.blank?
227
+ yield @col1 if block_given?
228
+ @col1
229
+ end
230
+
231
+ # Returns the second column of this tab.
232
+ def col2
233
+ @col2 = Col.new if @col2.blank?
234
+ yield @col2 if block_given?
235
+ @col2
236
+ end
237
+
238
+ def action(url, h={})
239
+ h[:url] = url
240
+ @actions << Action.new(h)
241
+ end
242
+ end
243
+
244
+ # Form may have up to two columns.
245
+ class Col
246
+ include Forma::FieldHelper
247
+ attr_reader :fields
248
+
249
+ def initialize(fields = [])
250
+ @fields = fields
251
+ end
252
+
253
+ def add_field(f)
254
+ @fields << f
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,139 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Forma
3
+ module Helpers
4
+ def forma_for(model, opts = {})
5
+ opts[:model] = model
6
+ opts[:edit] = true
7
+ opts[:auth_token] = form_authenticity_token if defined?(Rails)
8
+ opts[:method] = 'post' if opts[:method].blank?
9
+ f = Forma::Form.new(opts)
10
+ yield f if block_given?
11
+ f.to_html.to_s
12
+ end
13
+
14
+ def view_for(model, opts = {})
15
+ opts[:model] = model
16
+ opts[:edit] = false
17
+ f = Forma::Form.new(opts)
18
+ yield f if block_given?
19
+ f.to_html.to_s
20
+ end
21
+
22
+ def table_for(models, opts = {}, &block)
23
+ opts[:models] = models
24
+ opts[:context] = block
25
+ t = Forma::Table.new(opts)
26
+ yield t if block_given?
27
+ t.to_html.to_s
28
+ end
29
+
30
+ module_function :forma_for
31
+ module_function :view_for
32
+ end
33
+
34
+ module FieldHelper
35
+ def complex_field(opts = {})
36
+ field = Forma::ComplexField.new(opts)
37
+ yield field if block_given?
38
+ add_field(field)
39
+ end
40
+
41
+ def map_field(name, opts = {})
42
+ opts[:name] = name
43
+ field = Forma::MapField.new(opts)
44
+ yield field if block_given?
45
+ add_field(field)
46
+ end
47
+
48
+ def subform(name, opts = {})
49
+ opts[:name] = name
50
+ field = Forma::SubformField.new(opts)
51
+ yield field.form if block_given?
52
+ add_field(field)
53
+ end
54
+
55
+ def text_field(name, opts={})
56
+ opts[:name] = name
57
+ field = Forma::TextField.new(opts)
58
+ yield field if block_given?
59
+ add_field(field)
60
+ end
61
+
62
+ def password_field(name, opts={})
63
+ opts[:password] = true
64
+ text_field(name, opts)
65
+ end
66
+
67
+ def email_field(name, opts={})
68
+ opts[:name] = name
69
+ field = Forma::EmailField.new(opts)
70
+ yield field if block_given?
71
+ add_field(field)
72
+ end
73
+
74
+ def date_field(name, opts={})
75
+ opts[:name] = name
76
+ field = Forma::DateField.new(opts)
77
+ yield field if block_given?
78
+ add_field(field)
79
+ end
80
+
81
+ def boolean_field(name, opts={})
82
+ opts[:name] = name
83
+ field = Forma::BooleanField.new(opts)
84
+ yield field if block_given?
85
+ add_field(field)
86
+ end
87
+
88
+ def image_field(name, opts={})
89
+ opts[:name] = name
90
+ field = Forma::ImageField.new(opts)
91
+ yield field if block_given?
92
+ add_field(field)
93
+ end
94
+
95
+ def number_field(name, opts = {})
96
+ opts[:name] = name
97
+ field = Forma::NumberField.new(opts)
98
+ yield field if block_given?
99
+ add_field(field)
100
+ end
101
+
102
+ def combo_field(name, opts = {})
103
+ opts[:name] = name
104
+ field = Forma::ComboField.new(opts)
105
+ yield field if block_given?
106
+ add_field(field)
107
+ end
108
+
109
+ def select_field(name, search_url, opts = {})
110
+ opts[:name] = name
111
+ opts[:search_url] = search_url
112
+ field = Forma::SelectField.new(opts)
113
+ yield field if block_given?
114
+ add_field(field)
115
+ end
116
+ end
117
+
118
+ module WithTitleElement
119
+ def title_element
120
+ def active_title
121
+ el(
122
+ 'span',
123
+ attrs: { class: (self.collapsible ? ['ff-active-title', 'ff-collapsible'] : ['ff-active-title']) },
124
+ children: [
125
+ (el('i', attrs: { class: (self.collapsed ? ['ff-collapse', 'ff-collapsed'] : ['ff-collapse']) }) if self.collapsible),
126
+ (el('img', attrs: { src: self.icon }) if self.icon),
127
+ (el('span', text: self.title)),
128
+ ].reject { |x| x.blank? }
129
+ )
130
+ end
131
+ if self.title.present?
132
+ title_acts = el('div', attrs: { class: 'ff-title-actions' },
133
+ children: self.title_actions.map { |a| a.to_html(@model) }
134
+ ) if self.title_actions.any?
135
+ el('div', attrs: { class: 'ff-title' }, children: [ active_title, title_acts ])
136
+ end
137
+ end
138
+ end
139
+ end
data/lib/forma/html.rb CHANGED
@@ -1,3 +1,164 @@
1
1
  # -*- encoding : utf-8 -*-
2
- require 'forma/html/attributes'
3
- require 'forma/html/element'
2
+
3
+ # Utilities for html text generation.
4
+ module Forma::Html
5
+
6
+ # Attribute creation.
7
+ def attr(*params)
8
+ if params.length == 2
9
+ SimpleAttr.new(params[0].to_s, params[1].to_s)
10
+ elsif params.length == 1 and params[0].is_a?(Hash)
11
+ StyleAttr.new(params[0])
12
+ elsif params.length == 1
13
+ ClassAttr.new(params[0])
14
+ else
15
+ raise "illegal attr specification: #{params}"
16
+ end
17
+ end
18
+
19
+ # You can easily create elements using this method.
20
+ #
21
+ # ```
22
+ # include Forma::Html
23
+ # element = el("div", attrs = {id: 'main', class: 'header', style: {'font-size' => '20px'}})
24
+ # html = element.to_s
25
+ # ```
26
+ def el(tag, opts = {})
27
+ opts = opts.symbolize_keys
28
+ h = { text: opts[:text], html: opts[:html] }
29
+ if opts[:attrs]
30
+ attributes = []
31
+ opts[:attrs].each do |k, v|
32
+ if k == :class || k == :style
33
+ attributes << attr(v)
34
+ else
35
+ attributes << attr(k, v)
36
+ end
37
+ end
38
+ h[:attrs] = attributes
39
+ end
40
+ h[:children] = opts[:children]
41
+ Element.new(tag, h)
42
+ end
43
+
44
+ module_function :attr
45
+ module_function :el
46
+
47
+ private
48
+
49
+ class Attr; end
50
+
51
+ # Simple attribute.
52
+ class SimpleAttr < Attr
53
+ attr_reader :name, :value
54
+ def initialize(name, value)
55
+ @name = name
56
+ @value = value
57
+ end
58
+
59
+ def to_s
60
+ if @name.present? and @value.present?
61
+ %Q{#{@name}="#{@value}"}
62
+ end
63
+ end
64
+ end
65
+
66
+ # Class attribute.
67
+ class ClassAttr < Attr
68
+ attr_reader :values
69
+ def initialize(values)
70
+ @values = values
71
+ end
72
+
73
+ def to_s
74
+ if @values.present?
75
+ if @values.is_a?(Array)
76
+ %Q{class="#{@values.join(" ")}"}
77
+ else
78
+ %Q{class="#{@values}"}
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # Style attribute.
85
+ class StyleAttr < Attr
86
+ attr_reader :styles
87
+ def initialize(styles)
88
+ @styles = styles
89
+ end
90
+
91
+ def to_s
92
+ if @styles.present?
93
+ %Q{style="#{@styles.map{ |k,v| "#{k}:#{v}" }.join(";")}"}
94
+ end
95
+ end
96
+ end
97
+
98
+ # Element class.
99
+ class Element
100
+ attr_reader :tag, :id, :attrs
101
+ attr_accessor :text
102
+
103
+ def initialize(tag, h)
104
+ @tag = tag.to_s
105
+ if h[:text]; @text = h[:text]
106
+ elsif h[:html]; @text = h[:html].html_safe
107
+ end
108
+ @attrs = h[:attrs] || []
109
+ @children = []
110
+ h[:children].each { |c| @children << c } if h[:children]
111
+ ids = @attrs.select { |x| x.is_a?(SimpleAttr) and x.name == "id" }.map { |x| x.value }
112
+ @id = ids[0] if ids.length > 0
113
+ @classes = @attrs.select { |x| x.is_a?(ClassAttr) }.map{ |x| x.values }.flatten
114
+ end
115
+
116
+ def klass
117
+ @classes
118
+ end
119
+
120
+ def to_s
121
+ generate_html.html_safe
122
+ end
123
+
124
+ def attrs_by_name(name)
125
+ if name.to_s == 'class' then attrs.select { |x| x.is_a?(ClassAttr) }
126
+ elsif name.to_s == 'style' then attrs.select { |x| x.is_a?(SimpleAttr) }
127
+ else attrs.select { |x| (x.respond_to?(:name) and x.name == name.to_s) }
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def generate_html
134
+ h = ''
135
+ h << '<' << @tag << generate_tag_and_attributes.to_s << '>'
136
+ h << generate_inner_html
137
+ h << '</' << @tag << '>'
138
+ h
139
+ end
140
+
141
+ def generate_tag_and_attributes
142
+ attrs = @attrs.map{|a| a.to_s}.reject{|s| s.blank? }.join(" ")
143
+ ' ' << attrs unless attrs.blank?
144
+ end
145
+
146
+ def generate_inner_html
147
+ h = ''
148
+ if @text.html_safe?
149
+ h << @text
150
+ else
151
+ h << ERB::Util.html_escape(@text)
152
+ end
153
+ h << generate_children unless @children.blank?
154
+ h
155
+ end
156
+
157
+ def generate_children
158
+ h = ''
159
+ @children.each { |c| h << c.to_s }
160
+ h
161
+ end
162
+ end
163
+
164
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'forma/helpers'
3
+
4
+ module Forma
5
+
6
+ class Engine < ::Rails::Engine
7
+ end
8
+
9
+ class Railtie < Rails::Railtie
10
+ initializer 'forma.helpers' do
11
+ ActionView::Base.send :include, Forma::Helpers
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,115 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Forma
3
+ class Table
4
+ include Forma::Html
5
+ include Forma::FieldHelper
6
+ include Forma::WithTitleElement
7
+ attr_reader :collapsible, :collapsed, :icon, :title
8
+ attr_reader :title_actions
9
+
10
+ def initialize(h = {})
11
+ h = h.symbolize_keys
12
+ @id = h[:id]
13
+ # title properties
14
+ @title = h[:title]
15
+ @icon = h[:icon]
16
+ @collapsible = h[:collapsible]
17
+ @collapsed = h[:collapsed]
18
+ # values and fields
19
+ @models = h[:models]
20
+ @fields = h[:fields] || []
21
+ @paginate = h[:paginate]
22
+ # actions
23
+ @title_actions = h[:title_actions] || []
24
+ @item_actions = h[:item_actions] || []
25
+ # context
26
+ @context = h[:context]
27
+ end
28
+
29
+ def to_html
30
+ el(
31
+ 'div',
32
+ attrs: { id: @id, class: ['ff-table'] },
33
+ children: [
34
+ title_element,
35
+ body_element,
36
+ ]
37
+ )
38
+ end
39
+
40
+ def title_action(url, h={})
41
+ h[:url] = url
42
+ @title_actions << Action.new(h)
43
+ end
44
+
45
+ def item_action(url, h={})
46
+ h[:url] = url
47
+ @item_actions << Action.new(h)
48
+ end
49
+
50
+ def add_field(f)
51
+ @fields << f
52
+ end
53
+
54
+ def paginate(h={})
55
+ @paginate = true
56
+ @paginate_options = h
57
+ end
58
+
59
+ private
60
+
61
+ def body_element
62
+ el(
63
+ 'div', attrs: {
64
+ class: ['ff-table-body', 'ff-collapsible-body'],
65
+ style: ( {display: 'none'} if @collapsible && @collapsed ),
66
+ },
67
+ children: [ table_element, pagination_element ]
68
+ )
69
+ end
70
+
71
+ def table_element
72
+ def table_header_element
73
+ children = @fields.map { |f|
74
+ f.model = @models.first
75
+ label_text = f.localized_label
76
+ label_hint = f.localized_hint
77
+ el('th', attrs: { class: 'ff-field' }, text: label_text, children: [
78
+ (el('i', attrs: { class: 'ff-field-hint', 'data-toggle' => 'tooltip', title: label_hint }) if label_hint.present?)
79
+ ])
80
+ }
81
+ children << el('th', attrs: { style: {width: '100px'} }) if @item_actions.any?
82
+ el('thead', children: [
83
+ el('tr', children: children)
84
+ ])
85
+ end
86
+ def table_row(model)
87
+ children = @fields.map { |fld|
88
+ fld.model = model
89
+ el('td', children: [ fld.to_html(false) ])
90
+ }
91
+ if @item_actions.any?
92
+ children << el('td', children: @item_actions.map { |act| act.to_html(model) })
93
+ end
94
+ el('tr', children: children)
95
+ end
96
+ def table_body_element
97
+ children =
98
+ el('tbody', children: @models.map { |model| table_row(model) })
99
+ end
100
+ if @models and @models.any?
101
+ el('table', attrs: { class: 'ff-common-table' }, children: [ table_header_element, table_body_element ])
102
+ else
103
+ el('div', attrs: { class: ['ff-empty', 'ff-table-empty'] }, text: Forma.config.texts.table_empty)
104
+ end
105
+ end
106
+
107
+ def pagination_element
108
+ if @paginate and @context
109
+ self_from_block = eval("self", @context.binding)
110
+ s = self_from_block.send(:will_paginate, @models, @paginate_options)
111
+ el('div', html: s.to_s)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Forma
3
+ module Utils
4
+ def singular_name(model)
5
+ if model.respond_to?(:model_name); model.model_name.singular_route_key # Mongoid
6
+ elsif model.class.respond_to?(:table_name); model.class.table_name.singularize # ActiveModel
7
+ else; '' # Others
8
+ end
9
+ end
10
+
11
+ def extract_value(val, name)
12
+ def simple_value(model, name)
13
+ if model.respond_to?(name); model.send(name)
14
+ elsif model.respond_to?('[]'); model[name] || model[name.to_sym]
15
+ end
16
+ end
17
+ name.to_s.split('.').each { |n| val = simple_value(val, n) if val }
18
+ val
19
+ end
20
+
21
+ module_function :singular_name
22
+ module_function :extract_value
23
+ end
24
+ end