express_templates 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -5
- data/lib/express_templates/components.rb +2 -0
- data/lib/express_templates/components/capabilities/parenting.rb +1 -1
- data/lib/express_templates/components/capabilities/wrapping.rb +1 -1
- data/lib/express_templates/components/content_for.rb +5 -1
- data/lib/express_templates/components/for_each.rb +1 -1
- data/lib/express_templates/components/form_for.rb +375 -8
- data/lib/express_templates/components/table_for.rb +54 -10
- data/lib/express_templates/components/tree_for.rb +46 -5
- data/lib/express_templates/expander.rb +1 -5
- data/lib/express_templates/macro.rb +1 -0
- data/lib/express_templates/version.rb +1 -1
- data/test/components/container_test.rb +16 -1
- data/test/components/form_for_test.rb +197 -0
- data/test/components/table_for_test.rb +128 -33
- data/test/components/tree_for_test.rb +103 -0
- data/test/dummy/app/views/hello/show.html.et +1 -1
- data/test/dummy/config/environments/production.rb +1 -1
- data/test/dummy/config/environments/test.rb +2 -1
- data/test/dummy/log/test.log +558 -146934
- data/test/dummy/test/controllers/hello_controller_test.rb +1 -1
- data/test/dummy/tmp/cache/assets/test/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/test/test_helper.rb +2 -0
- metadata +13 -9
- data/test/dummy/log/development.log +0 -6478
@@ -8,17 +8,19 @@ module ExpressTemplates
|
|
8
8
|
#
|
9
9
|
# Example:
|
10
10
|
#
|
11
|
-
#
|
11
|
+
# ````ruby
|
12
12
|
# table_for(:people) do |t|
|
13
13
|
# t.column :name
|
14
14
|
# t.column :email
|
15
15
|
# t.column :phone
|
16
|
+
# t.column :hourly_rate, header: "Rate",
|
17
|
+
# formatter: -> (amount) {'$%0.2f' % amount rescue 'N/A'}
|
16
18
|
# end
|
17
|
-
#
|
19
|
+
# ````
|
18
20
|
#
|
19
21
|
# This assumes that a @people variable will exist in the
|
20
22
|
# view and that it will be a collection whose members respond to
|
21
|
-
# :name, :email,
|
23
|
+
# :name, :email, :phone, :hourly_rate
|
22
24
|
#
|
23
25
|
# This will result in markup like the following:
|
24
26
|
#
|
@@ -28,6 +30,7 @@ module ExpressTemplates
|
|
28
30
|
# <th class="name">Name</th>
|
29
31
|
# <th class="email">Email</th>
|
30
32
|
# <th class="phone">Phone</th>
|
33
|
+
# <th class="hourly_rate">Rate</th>
|
31
34
|
# </tr>
|
32
35
|
# </thead>
|
33
36
|
# <tbody>
|
@@ -35,10 +38,17 @@ module ExpressTemplates
|
|
35
38
|
# <td class="name">Steven Talcott Smith</td>
|
36
39
|
# <td class="email">steve@aelogica.com</td>
|
37
40
|
# <td class="phone">415-555-1212</td>
|
41
|
+
# <td class="hourly_rate">$250.00</td>
|
38
42
|
# </tr>
|
39
43
|
# </tbody>
|
40
44
|
# </table>
|
41
45
|
#
|
46
|
+
# Note that column options include :formatter and :header.
|
47
|
+
#
|
48
|
+
# :formatter may be a stabby lambda which is passed the value to be formatted.
|
49
|
+
#
|
50
|
+
# :header may be either a string
|
51
|
+
#
|
42
52
|
class TableFor < Base
|
43
53
|
include Capabilities::Configurable
|
44
54
|
include Capabilities::Building
|
@@ -68,15 +78,21 @@ module ExpressTemplates
|
|
68
78
|
}
|
69
79
|
}
|
70
80
|
tbody {
|
71
|
-
for_each(my[:id]) {
|
81
|
+
for_each("@#{my[:id]}".to_sym) {
|
72
82
|
|
73
|
-
tr(id:
|
83
|
+
tr(id: "#{my[:id].to_s.singularize}-{{#{my[:id].to_s.singularize}.id}}",
|
74
84
|
class: my[:id].to_s.singularize) {
|
75
85
|
|
76
86
|
columns.each do |column|
|
77
|
-
|
78
|
-
column.
|
79
|
-
|
87
|
+
if(column.has_actions?)
|
88
|
+
td(class: column.name) {
|
89
|
+
column.show_actions(my[:id].to_s)
|
90
|
+
}
|
91
|
+
else
|
92
|
+
td(class: column.name) {
|
93
|
+
column.format(my[:id].to_s.singularize)
|
94
|
+
}
|
95
|
+
end
|
80
96
|
end
|
81
97
|
}
|
82
98
|
}
|
@@ -98,6 +114,12 @@ module ExpressTemplates
|
|
98
114
|
@name = name
|
99
115
|
@options = options
|
100
116
|
@formatter = options[:formatter]
|
117
|
+
@header = options[:header]
|
118
|
+
@actions = options[:actions] || []
|
119
|
+
end
|
120
|
+
|
121
|
+
def has_actions?
|
122
|
+
@actions.any?
|
101
123
|
end
|
102
124
|
|
103
125
|
def format(item_name)
|
@@ -108,12 +130,34 @@ module ExpressTemplates
|
|
108
130
|
end
|
109
131
|
end
|
110
132
|
|
133
|
+
def show_actions(item_name)
|
134
|
+
action_links = StringIO.new
|
135
|
+
@actions.each do |action|
|
136
|
+
action_name = action.to_s
|
137
|
+
if action_name.eql?('edit')
|
138
|
+
action_links.puts "<a href='/#{item_name}/{{#{item_name.singularize}.id}}/edit'>Edit</a>"
|
139
|
+
elsif action_name.eql?('delete')
|
140
|
+
action_links.puts "<a href='/#{item_name}/{{#{item_name.singularize}.id}}' data-method='delete' data-confirm='Are you sure?'>Delete</a>"
|
141
|
+
elsif action_name.eql?('show')
|
142
|
+
action_links.puts "<a href='/#{item_name}/{{#{item_name.singularize}.id}}'>Show</a>"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
action_links.string
|
146
|
+
end
|
147
|
+
|
111
148
|
def title
|
112
|
-
|
149
|
+
case
|
150
|
+
when @header.nil?
|
151
|
+
@name.to_s.try(:titleize)
|
152
|
+
when @header.kind_of?(String)
|
153
|
+
@header
|
154
|
+
when @header.kind_of?(Proc)
|
155
|
+
"{{(#{@header.source}).call(#{@name})}}"
|
156
|
+
end
|
113
157
|
end
|
114
158
|
end
|
115
159
|
|
116
160
|
end
|
117
161
|
|
118
162
|
end
|
119
|
-
end
|
163
|
+
end
|
@@ -22,20 +22,61 @@ module ExpressTemplates
|
|
22
22
|
# <ul id="roles" class="roles tree">
|
23
23
|
# <li>SuperAdmin
|
24
24
|
# <ul>
|
25
|
-
# <li>Admin
|
25
|
+
# <li>Admin
|
26
26
|
# <ul>
|
27
|
-
# <li>Publisher
|
27
|
+
# <li>Publisher
|
28
28
|
# <ul>
|
29
29
|
# <li>Author</li>
|
30
30
|
# </ul>
|
31
|
+
# </li>
|
31
32
|
# <li>Auditor</li>
|
32
|
-
# </
|
33
|
+
# </ul>
|
33
34
|
# </li>
|
34
|
-
# </
|
35
|
+
# </ul>
|
35
36
|
# </li>
|
36
|
-
# </
|
37
|
+
# </ul>
|
37
38
|
#
|
38
39
|
class TreeFor < Container
|
40
|
+
def node_renderer
|
41
|
+
return (-> (node, renderer) {
|
42
|
+
ExpressTemplates::Indenter.for(:tree) do |ws, wsnl|
|
43
|
+
"#{wsnl}<li>"+
|
44
|
+
_yield +
|
45
|
+
if node.children.any?
|
46
|
+
ExpressTemplates::Indenter.for(:tree) do |ws, wsnl|
|
47
|
+
"#{wsnl}<ul>" +
|
48
|
+
node.children.map do |child|
|
49
|
+
renderer.call(child, renderer)
|
50
|
+
end.join +
|
51
|
+
"#{wsnl}</ul>"
|
52
|
+
end +
|
53
|
+
"#{wsnl}</li>"
|
54
|
+
else
|
55
|
+
"</li>"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
}).source.sub(/\W_yield\W/, compile_children.lstrip)
|
59
|
+
end
|
60
|
+
|
61
|
+
def compile
|
62
|
+
collection = _variablize(@options[:id])
|
63
|
+
member = @options[:id].to_s.singularize
|
64
|
+
return 'ExpressTemplates::Components::TreeFor.render_in(self) {
|
65
|
+
node_renderer = '+node_renderer.gsub(/node/, member)+'
|
66
|
+
ExpressTemplates::Indenter.for(:tree) do |ws, wsnl|
|
67
|
+
"#{ws}<ul id=\"roles\" class=\"roles tree\">" +
|
68
|
+
'+collection+'.map do |'+member+'|
|
69
|
+
node_renderer.call('+member+', node_renderer)
|
70
|
+
end.join +
|
71
|
+
"#{wsnl}</ul>\n"
|
72
|
+
end
|
73
|
+
}'
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def _variablize(sym)
|
78
|
+
"@#{sym}"
|
79
|
+
end
|
39
80
|
end
|
40
81
|
end
|
41
82
|
end
|
@@ -21,7 +21,7 @@ module ExpressTemplates
|
|
21
21
|
def expand(source=nil, &block)
|
22
22
|
case
|
23
23
|
when block.nil? && source
|
24
|
-
modified =
|
24
|
+
modified = _replace_yield_with_yielder(source)
|
25
25
|
instance_eval(modified, @template.inspect)
|
26
26
|
when block
|
27
27
|
instance_exec &block
|
@@ -92,10 +92,6 @@ module ExpressTemplates
|
|
92
92
|
source.gsub(/(\W)(yield)(\([^\)]*\))?/, '\1 (stack << ExpressTemplates::Markup::Yielder.new\3)')
|
93
93
|
end
|
94
94
|
|
95
|
-
def _wrap_instance_vars(source)
|
96
|
-
source.gsub(/(\W)(@\w+)(\W)?/, '\1 (stack << ExpressTemplates::Markup::Wrapper.new("\2") )\3')
|
97
|
-
end
|
98
|
-
|
99
95
|
class Stack
|
100
96
|
def initialize
|
101
97
|
@stack = [[]]
|
@@ -38,7 +38,7 @@ class ContainerTest < ActiveSupport::TestCase
|
|
38
38
|
child2.verify
|
39
39
|
end
|
40
40
|
|
41
|
-
test "
|
41
|
+
test "renders children in place of _yield" do
|
42
42
|
container = TestContainer.new
|
43
43
|
child1, child2 = mock_children
|
44
44
|
container.children = [child1, child2]
|
@@ -46,4 +46,19 @@ class ContainerTest < ActiveSupport::TestCase
|
|
46
46
|
assert_equal "<p>onetwo</p>", eval(container.compile)
|
47
47
|
end
|
48
48
|
|
49
|
+
class Context
|
50
|
+
def a_helper
|
51
|
+
"foo"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
test "children with interpolations" do
|
56
|
+
markup = ExpressTemplates.render(Context.new) do
|
57
|
+
row {
|
58
|
+
p %q(Should say: {{a_helper}}.)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
assert_equal "<div class=\"row\"><p>Should say: foo.</p></div>", markup
|
62
|
+
end
|
63
|
+
|
49
64
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
class FormForTest < ActiveSupport::TestCase
|
5
|
+
class Context
|
6
|
+
def initialize(resource)
|
7
|
+
@resource = resource
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def resource
|
12
|
+
OpenStruct.new(
|
13
|
+
id: 1,
|
14
|
+
name: 'Foo',
|
15
|
+
body: 'Hello world',
|
16
|
+
email: 'some@email.com',
|
17
|
+
phone: '123123123',
|
18
|
+
url: 'http://someurl.com',
|
19
|
+
number: 123,
|
20
|
+
dropdown: 'yes',
|
21
|
+
gender: 'Male'
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def setup
|
26
|
+
@example_compiled = -> {
|
27
|
+
ExpressTemplates::Components::FormFor.render_in(self) {
|
28
|
+
"<form action=\"/resources\" method=\"put\" url=\"/yolo\">
|
29
|
+
<div style=\"display:none\">
|
30
|
+
"+%Q(#{utf8_enforcer_tag})+%Q(#{method_tag(:post)})+%Q(#{token_tag})+"
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<div class=\"\">
|
34
|
+
"+%Q(#{label_tag(:name, "post title")})+%Q(#{text_field_tag(:name, @resource.name, label: "post title")})+"
|
35
|
+
</div>
|
36
|
+
|
37
|
+
<div class=\"\">
|
38
|
+
"+%Q(#{label_tag(:body, nil)})+%Q(#{text_field_tag(:body, @resource.body, class: "string")})+"
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div class=\"field input\">
|
42
|
+
"+%Q(#{label_tag(:email, nil)})+%Q(#{email_field_tag(:email, @resource.email, wrapper_class: "field input")})+"
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<div class=\"\">
|
46
|
+
"+%Q(#{label_tag(:phone, nil)})+%Q(#{phone_field_tag(:phone, @resource.phone, {})})+"
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<div class=\"\">
|
50
|
+
"+%Q(#{label_tag(:url, nil)})+%Q(#{url_field_tag(:url, @resource.url, {})})+"
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<div class=\"\">
|
54
|
+
"+%Q(#{label_tag(:number, nil)})+%Q(#{number_field_tag(:number, @resource.number, {})})+"
|
55
|
+
</div>
|
56
|
+
|
57
|
+
<div class=\"\">"+%Q(#{submit_tag("Save it!", {})})+"</div>
|
58
|
+
</form>
|
59
|
+
"
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def example_compiled_src
|
65
|
+
# necessary because the #source method is not perfect yet
|
66
|
+
# ideally we would have #source_body
|
67
|
+
@example_compiled.source_body
|
68
|
+
end
|
69
|
+
|
70
|
+
def simple_form(resource)
|
71
|
+
ctx = Context.new(resource)
|
72
|
+
fragment = -> {
|
73
|
+
form_for(:resource, method: :put, url: '/yolo') do |f|
|
74
|
+
f.text_field :name, label: 'post title'
|
75
|
+
f.text_field :body, class: 'string'
|
76
|
+
f.email_field :email, wrapper_class: 'field input'
|
77
|
+
f.phone_field :phone
|
78
|
+
f.url_field :url
|
79
|
+
f.number_field :number
|
80
|
+
f.submit 'Save it!'
|
81
|
+
end
|
82
|
+
}
|
83
|
+
return ctx, fragment
|
84
|
+
end
|
85
|
+
|
86
|
+
def select_form(resource)
|
87
|
+
ctx = Context.new(resource)
|
88
|
+
fragment = -> {
|
89
|
+
form_for(:resource, method: :put) do |f|
|
90
|
+
f.select :dropdown, ['yes', 'no'], selected: 'yes'
|
91
|
+
f.select :dropdown, '{{ options_from_collection_for_select(@choices, "id", "name") }}'
|
92
|
+
end
|
93
|
+
}
|
94
|
+
return ctx, fragment
|
95
|
+
end
|
96
|
+
|
97
|
+
def radio_form(resource)
|
98
|
+
ctx = Context.new(resource)
|
99
|
+
fragment = -> {
|
100
|
+
form_for(:resource) do |f|
|
101
|
+
f.radio :age, [[1, 'One'],[2, 'Two']], :first, :last
|
102
|
+
end
|
103
|
+
}
|
104
|
+
return ctx, fragment
|
105
|
+
end
|
106
|
+
|
107
|
+
def checkbox_form(resource)
|
108
|
+
ctx = Context.new(resource)
|
109
|
+
fragment = -> {
|
110
|
+
form_for(:resource, method: :put) do |f|
|
111
|
+
f.checkbox :age, [[1, 'One'], [2, 'Two']], :first, :last
|
112
|
+
end
|
113
|
+
}
|
114
|
+
return ctx, fragment
|
115
|
+
end
|
116
|
+
|
117
|
+
test "fields compiled source is legible and transparent" do
|
118
|
+
ExpressTemplates::Markup::Tag.formatted do
|
119
|
+
# ctx, fragment = simple_form(resource)
|
120
|
+
# assert_equal example_compiled_src, ExpressTemplates.compile(&fragment)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
test "select compiled source is legible and transparent" do
|
125
|
+
@example_compiled = -> {
|
126
|
+
ExpressTemplates::Components::FormFor.render_in(self) {
|
127
|
+
"<form action=\"/resources/#{@resource.id}\" method=\"post\">
|
128
|
+
<div style=\"display:none\">
|
129
|
+
"+%Q(#{utf8_enforcer_tag})+%Q(#{method_tag(:patch)})+%Q(#{token_tag})+"
|
130
|
+
</div>
|
131
|
+
|
132
|
+
<div class=\"\">
|
133
|
+
"+%Q(#{label_tag("resource_dropdown", "Dropdown")})+%Q(#{select_tag("resource[dropdown]", '<option selected=selected>yes</option><option >no</option>'.html_safe, {})})+"
|
134
|
+
</div>
|
135
|
+
|
136
|
+
<div class=\"\">
|
137
|
+
"+%Q(#{label_tag("resource_dropdown", "Dropdown")})+%Q(#{select_tag("resource[dropdown]", options_from_collection_for_select(@choices, "id", "name") , {})})+"
|
138
|
+
</div>
|
139
|
+
</form>
|
140
|
+
"
|
141
|
+
}
|
142
|
+
}
|
143
|
+
ExpressTemplates::Markup::Tag.formatted do
|
144
|
+
ctx, fragment = select_form(resource)
|
145
|
+
assert_equal example_compiled_src, ExpressTemplates.compile(&fragment)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
test "radio compiled source is legible and transparent" do
|
150
|
+
@example_compiled = -> {
|
151
|
+
ExpressTemplates::Components::FormFor.render_in(self) {
|
152
|
+
"<form action=\"/resources\" method=\"post\">
|
153
|
+
<div style=\"display:none\">
|
154
|
+
"+%Q(#{utf8_enforcer_tag})+%Q(#{method_tag(:post)})+%Q(#{token_tag})+"
|
155
|
+
</div>
|
156
|
+
|
157
|
+
<div class=\"\">
|
158
|
+
"+%Q(#{collection_radio_buttons(:resource, :age, [[1, "One"], [2, "Two"]], :first, :last, {}) do |b|
|
159
|
+
b.label(class: 'radio') { b.radio_button + b.text }
|
160
|
+
end})+"
|
161
|
+
</div>
|
162
|
+
</form>
|
163
|
+
"
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
ExpressTemplates::Markup::Tag.formatted do
|
168
|
+
ctx, fragment = radio_form(resource)
|
169
|
+
assert_equal example_compiled_src, ExpressTemplates.compile(&fragment)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
test "checkbox compiled source is legible and transparent" do
|
174
|
+
@example_compiled = -> {
|
175
|
+
ExpressTemplates::Components::FormFor.render_in(self) {
|
176
|
+
"<form action=\"/resources/#{@resource.id}\" method=\"post\">
|
177
|
+
<div style=\"display:none\">
|
178
|
+
"+%Q(#{utf8_enforcer_tag})+%Q(#{method_tag(:patch)})+%Q(#{token_tag})+"
|
179
|
+
</div>
|
180
|
+
|
181
|
+
<div class=\"\">
|
182
|
+
"+%Q(#{collection_check_boxes(:resource, :age, [[1, "One"], [2, "Two"]], :first, :last, {}) do |b|
|
183
|
+
b.label(class: 'checkbox') { b.check_box + b.text }
|
184
|
+
end})+"
|
185
|
+
</div>
|
186
|
+
</form>
|
187
|
+
"
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
ExpressTemplates::Markup::Tag.formatted do
|
192
|
+
ctx, fragment = checkbox_form(resource)
|
193
|
+
assert_equal example_compiled_src, ExpressTemplates.compile(&fragment)
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|