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/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