brut 0.0.13 → 0.0.20
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -6
- data/brut.gemspec +1 -3
- data/lib/brut/back_end.rb +7 -0
- data/lib/brut/cli/apps/scaffold.rb +16 -24
- data/lib/brut/framework/config.rb +4 -43
- data/lib/brut/framework/mcp.rb +1 -1
- data/lib/brut/front_end/asset_path_resolver.rb +15 -0
- data/lib/brut/front_end/component.rb +66 -234
- data/lib/brut/front_end/components/constraint_violations.rb +9 -9
- data/lib/brut/front_end/components/form_tag.rb +16 -28
- data/lib/brut/front_end/components/i18n_translations.rb +12 -13
- data/lib/brut/front_end/components/input.rb +0 -1
- data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -2
- data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
- data/lib/brut/front_end/components/inputs/select.rb +13 -20
- data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
- data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
- data/lib/brut/front_end/components/locale_detection.rb +2 -2
- data/lib/brut/front_end/components/page_identifier.rb +3 -5
- data/lib/brut/front_end/components/{time.rb → time_tag.rb} +13 -10
- data/lib/brut/front_end/components/traceparent.rb +5 -6
- data/lib/brut/front_end/http_method.rb +4 -0
- data/lib/brut/front_end/inline_svg_locator.rb +21 -0
- data/lib/brut/front_end/layout.rb +3 -0
- data/lib/brut/front_end/page.rb +16 -29
- data/lib/brut/front_end/request_context.rb +13 -0
- data/lib/brut/front_end/routing.rb +3 -2
- data/lib/brut/front_end.rb +41 -0
- data/lib/brut/i18n/base_methods.rb +14 -8
- data/lib/brut/i18n/for_back_end.rb +5 -0
- data/lib/brut/i18n/for_cli.rb +2 -1
- data/lib/brut/i18n/for_html.rb +9 -1
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/sinatra_helpers.rb +12 -7
- data/lib/brut/spec_support/component_support.rb +9 -9
- data/lib/brut/spec_support/e2e_support.rb +4 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +5 -0
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/spec_support.rb +4 -3
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +2 -46
- metadata +13 -41
- data/lib/brut/front_end/template.rb +0 -47
- data/lib/brut/front_end/templates/block_filter.rb +0 -61
- data/lib/brut/front_end/templates/erb_engine.rb +0 -26
- data/lib/brut/front_end/templates/erb_parser.rb +0 -84
- data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
- data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
- data/lib/brut/front_end/templates/locator.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a6e2324291c72fa3b3012b38b604104ce6d00af875326c20df00cfc42d16a9c
|
4
|
+
data.tar.gz: c16981e60940c676e21590f9a3db5845c8cc77ee2c7dd86eba9cfb8777ac7a32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b41d3231c6e589b06deb3f668ff42983bf1021757961e1edf5c0b040f8e64dc4c4d4e481bf9feb34a3104371eecd11e5e0a63414a221f8748eca4982cebccec9
|
7
|
+
data.tar.gz: 5896d743754d4e9538a85268a3b72856b3b07ca940798e3732db577d67691fc819b7e927aa2a15a309c4440c7ea10beca45df5473d9ca7247b75e6f83678e4e8
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
brut (0.0.
|
4
|
+
brut (0.0.20)
|
5
5
|
concurrent-ruby
|
6
6
|
i18n
|
7
7
|
irb
|
@@ -9,15 +9,13 @@ PATH
|
|
9
9
|
opentelemetry-exporter-otlp
|
10
10
|
opentelemetry-sdk
|
11
11
|
ostruct
|
12
|
+
phlex
|
12
13
|
prism
|
13
14
|
rack-protection
|
14
15
|
rackup
|
15
|
-
rexml
|
16
16
|
semantic_logger
|
17
17
|
sequel
|
18
18
|
sinatra
|
19
|
-
temple
|
20
|
-
tilt
|
21
19
|
tzinfo
|
22
20
|
tzinfo-data
|
23
21
|
zeitwerk
|
@@ -114,6 +112,8 @@ GEM
|
|
114
112
|
opentelemetry-semantic_conventions (1.10.1)
|
115
113
|
opentelemetry-api (~> 1.0)
|
116
114
|
ostruct (0.6.1)
|
115
|
+
phlex (2.2.1)
|
116
|
+
zeitwerk (~> 2.7)
|
117
117
|
pp (0.6.2)
|
118
118
|
prettyprint
|
119
119
|
prettyprint (0.2.0)
|
@@ -136,7 +136,6 @@ GEM
|
|
136
136
|
psych (>= 4.0.0)
|
137
137
|
reline (0.6.0)
|
138
138
|
io-console (~> 0.5)
|
139
|
-
rexml (3.3.9)
|
140
139
|
rspec (3.13.0)
|
141
140
|
rspec-core (~> 3.13.0)
|
142
141
|
rspec-expectations (~> 3.13.0)
|
@@ -163,7 +162,6 @@ GEM
|
|
163
162
|
rack-session (>= 2.0.0, < 3)
|
164
163
|
tilt (~> 2.0)
|
165
164
|
stringio (3.1.3)
|
166
|
-
temple (0.10.3)
|
167
165
|
tilt (2.4.0)
|
168
166
|
tzinfo (2.0.6)
|
169
167
|
concurrent-ruby (~> 1.0)
|
data/brut.gemspec
CHANGED
@@ -39,15 +39,13 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_runtime_dependency "concurrent-ruby"
|
40
40
|
spec.add_runtime_dependency "i18n"
|
41
41
|
spec.add_runtime_dependency "nokogiri"
|
42
|
+
spec.add_runtime_dependency "phlex"
|
42
43
|
spec.add_runtime_dependency "prism"
|
43
44
|
spec.add_runtime_dependency "rack-protection"
|
44
45
|
spec.add_runtime_dependency "rackup"
|
45
|
-
spec.add_runtime_dependency "rexml"
|
46
46
|
spec.add_runtime_dependency "semantic_logger"
|
47
47
|
spec.add_runtime_dependency "sequel"
|
48
48
|
spec.add_runtime_dependency "sinatra"
|
49
|
-
spec.add_runtime_dependency "temple"
|
50
|
-
spec.add_runtime_dependency "tilt"
|
51
49
|
spec.add_runtime_dependency "tzinfo"
|
52
50
|
spec.add_runtime_dependency "tzinfo-data"
|
53
51
|
spec.add_runtime_dependency "zeitwerk"
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# The _back end_ of a Brut app is where your app's business logic and database are managed. While the bulk of your Brut app's code
|
2
|
+
# will be in the back end, Brut is far less prescriptive about how to manage that than it is the front end.
|
3
|
+
module Brut::BackEnd
|
4
|
+
autoload(:Validators, "brut/back_end/validator")
|
5
|
+
autoload(:Sidekiq, "brut/back_end/sidekiq")
|
6
|
+
# Do not put SeedData here - it must be loaded only when needed
|
7
|
+
end
|
@@ -184,7 +184,7 @@ end}
|
|
184
184
|
end
|
185
185
|
|
186
186
|
class Component < Brut::CLI::Command
|
187
|
-
description "Create a new component
|
187
|
+
description "Create a new component and associated test"
|
188
188
|
detailed_description "New components go in the `components/` folder of your app, however using --page will create a 'page private' component. To do that, the component name must be an inner class of an existing page, for example HomePage::Welcome. This component goes in a sub-folder inside the `pages/` area of your app"
|
189
189
|
opts.on("--page","If set, this component is for a specific page and won't go with the other components")
|
190
190
|
args "ComponentName"
|
@@ -217,12 +217,10 @@ end}
|
|
217
217
|
end
|
218
218
|
|
219
219
|
source_path = Pathname( (components_src_dir / relative_path).to_s + ".rb" )
|
220
|
-
html_source_path = Pathname( (components_src_dir / relative_path).to_s + ".html.erb" )
|
221
220
|
spec_path = Pathname( (components_specs_dir / relative_path).to_s + ".spec.rb" )
|
222
221
|
|
223
222
|
exists = [
|
224
223
|
source_path,
|
225
|
-
html_source_path,
|
226
224
|
spec_path,
|
227
225
|
].select(&:exist?)
|
228
226
|
|
@@ -236,22 +234,21 @@ end}
|
|
236
234
|
|
237
235
|
if global_options.dry_run?
|
238
236
|
out.puts "FileUtils.mkdir_p #{source_path.dirname}"
|
239
|
-
out.puts "FileUtils.mkdir_p #{html_source_path.dirname}"
|
240
237
|
out.puts "FileUtils.mkdir_p #{spec_path.dirname}"
|
241
238
|
else
|
242
239
|
FileUtils.mkdir_p source_path.dirname
|
243
|
-
FileUtils.mkdir_p html_source_path.dirname
|
244
240
|
FileUtils.mkdir_p spec_path.dirname
|
245
241
|
|
246
242
|
File.open(source_path,"w") do |file|
|
247
243
|
file.puts %{class #{class_name} < AppComponent
|
248
244
|
def initialize
|
249
245
|
end
|
246
|
+
|
247
|
+
def view_template
|
248
|
+
h2 { "Welcome to your new template" }
|
249
|
+
end
|
250
250
|
end}
|
251
251
|
end
|
252
|
-
File.open(html_source_path,"w") do |file|
|
253
|
-
file.puts "<h1>#{class_name} is ready!</h1>"
|
254
|
-
end
|
255
252
|
File.open(spec_path,"w") do |file|
|
256
253
|
file.puts %{require "spec_helper"
|
257
254
|
|
@@ -262,9 +259,8 @@ RSpec.describe #{class_name} do
|
|
262
259
|
end}
|
263
260
|
end
|
264
261
|
end
|
265
|
-
out.puts "Component source is in
|
266
|
-
out.puts "Component
|
267
|
-
out.puts "Component test is in #{spec_path.relative_path_from(Brut.container.project_root)}"
|
262
|
+
out.puts "Component source is in #{source_path.relative_path_from(Brut.container.project_root)}"
|
263
|
+
out.puts "Component test is in #{spec_path.relative_path_from(Brut.container.project_root)}"
|
268
264
|
0
|
269
265
|
end
|
270
266
|
end
|
@@ -283,7 +279,7 @@ end}
|
|
283
279
|
end
|
284
280
|
end
|
285
281
|
end
|
286
|
-
description "Create a new page
|
282
|
+
description "Create a new page and associated test"
|
287
283
|
args "page_route"
|
288
284
|
def execute
|
289
285
|
if args.length != 1
|
@@ -299,14 +295,12 @@ end}
|
|
299
295
|
i18n_locales_dir = Brut.container.i18n_locales_dir
|
300
296
|
|
301
297
|
page_source_path = Pathname( (pages_src_dir / page_relative_path).to_s + ".rb" )
|
302
|
-
template_source_path = Pathname( (pages_src_dir / page_relative_path).to_s + ".html.erb" )
|
303
298
|
page_spec_path = Pathname( (pages_specs_dir / page_relative_path).to_s + ".spec.rb" )
|
304
299
|
app_path = Pathname( Brut.container.app_src_dir / "app.rb" )
|
305
300
|
app_translations = Pathname( i18n_locales_dir / "en" / "2_app.rb")
|
306
301
|
|
307
302
|
exists = [
|
308
303
|
page_source_path,
|
309
|
-
template_source_path,
|
310
304
|
page_spec_path,
|
311
305
|
].select(&:exist?)
|
312
306
|
|
@@ -319,7 +313,6 @@ end}
|
|
319
313
|
end
|
320
314
|
|
321
315
|
FileUtils.mkdir_p page_source_path.dirname, noop: global_options.dry_run?
|
322
|
-
FileUtils.mkdir_p template_source_path.dirname, noop: global_options.dry_run?
|
323
316
|
FileUtils.mkdir_p page_spec_path.dirname, noop: global_options.dry_run?
|
324
317
|
|
325
318
|
route_code = "page \"#{route.path_template}\""
|
@@ -334,8 +327,11 @@ end}
|
|
334
327
|
page_class_code = %{class #{page_class_name} < AppPage
|
335
328
|
def initialize#{initializer_params_code} # add needed arguments here
|
336
329
|
end
|
330
|
+
|
331
|
+
def page_template
|
332
|
+
h1 { "#{page_class_name} is ready!" }
|
333
|
+
end
|
337
334
|
end}
|
338
|
-
template_code = %{<h1>#{page_class_name} is ready!</h1>}
|
339
335
|
page_spec_code = %{require "spec_helper"
|
340
336
|
|
341
337
|
RSpec.describe #{page_class_name} do
|
@@ -352,8 +348,6 @@ end}
|
|
352
348
|
out.puts "will contain:\n\n#{route_code}\n\n"
|
353
349
|
out.puts page_source_path.relative_path_from(Brut.container.project_root)
|
354
350
|
out.puts "will contain:\n\n#{page_class_code}\n\n"
|
355
|
-
out.puts template_source_path.relative_path_from(Brut.container.project_root)
|
356
|
-
out.puts "will contain:\n\n#{template_code}\n\n"
|
357
351
|
out.puts page_spec_path.relative_path_from(Brut.container.project_root)
|
358
352
|
out.puts "will contain:\n\n#{page_spec_code}\n\n"
|
359
353
|
out.puts app_translations.relative_path_from(Brut.container.project_root)
|
@@ -361,7 +355,6 @@ end}
|
|
361
355
|
else
|
362
356
|
|
363
357
|
File.open(page_source_path,"w") { it.puts page_class_code }
|
364
|
-
File.open(template_source_path,"w") { it.puts template_code }
|
365
358
|
File.open(page_spec_path,"w") { it.puts page_spec_code }
|
366
359
|
|
367
360
|
existing_translations = File.read(app_translations).split(/\n/)
|
@@ -393,11 +386,10 @@ end}
|
|
393
386
|
out.puts "Please make sure everything is correct. Here is the defintion that was not inserted:\n\n#{route_code}"
|
394
387
|
end
|
395
388
|
end
|
396
|
-
out.puts "Page source is in
|
397
|
-
out.puts "Page
|
398
|
-
out.puts "
|
399
|
-
out.puts "Added
|
400
|
-
out.puts "Added route to #{app_path.relative_path_from(Brut.container.project_root)}"
|
389
|
+
out.puts "Page source is in #{page_source_path.relative_path_from(Brut.container.project_root)}"
|
390
|
+
out.puts "Page test is in #{page_spec_path.relative_path_from(Brut.container.project_root)}"
|
391
|
+
out.puts "Added title to #{app_translations.relative_path_from(Brut.container.project_root)}"
|
392
|
+
out.puts "Added route to #{app_path.relative_path_from(Brut.container.project_root)}"
|
401
393
|
0
|
402
394
|
end
|
403
395
|
end
|
@@ -238,21 +238,6 @@ class Brut::Framework::Config
|
|
238
238
|
config_dir / "asset_metadata.json"
|
239
239
|
end
|
240
240
|
|
241
|
-
|
242
|
-
c.store(
|
243
|
-
"layout_locator",
|
244
|
-
"Brut::FrontEnd::Templates::Locator",
|
245
|
-
"Object to use to locate templates for layouts"
|
246
|
-
) do |layouts_src_dir,project_env,brut_internal_dir|
|
247
|
-
paths = if project_env.development?
|
248
|
-
[ layouts_src_dir, brut_internal_dir / "lib" / "brut" / "front_end" / "layouts" ]
|
249
|
-
else
|
250
|
-
layouts_src_dir
|
251
|
-
end
|
252
|
-
Brut::FrontEnd::Templates::Locator.new(paths: paths,
|
253
|
-
extension: "html.erb")
|
254
|
-
end
|
255
|
-
|
256
241
|
c.store_required_path(
|
257
242
|
"brut_internal_dir",
|
258
243
|
"Location to where the Brut gem is installed."
|
@@ -260,44 +245,20 @@ class Brut::Framework::Config
|
|
260
245
|
(Pathname(__FILE__).dirname / ".." / ".." / "..").expand_path
|
261
246
|
end
|
262
247
|
|
263
|
-
c.store(
|
264
|
-
"page_locator",
|
265
|
-
"Brut::FrontEnd::Templates::Locator",
|
266
|
-
"Object to use to locate templates for pages"
|
267
|
-
) do |pages_src_dir,project_env,brut_internal_dir|
|
268
|
-
paths = if project_env.development?
|
269
|
-
[ pages_src_dir, brut_internal_dir / "lib" / "brut" / "front_end" / "pages" ]
|
270
|
-
else
|
271
|
-
pages_src_dir
|
272
|
-
end
|
273
|
-
Brut::FrontEnd::Templates::Locator.new(paths: paths,
|
274
|
-
extension: "html.erb")
|
275
|
-
end
|
276
|
-
|
277
|
-
c.store(
|
278
|
-
"component_locator",
|
279
|
-
"Brut::FrontEnd::Templates::Locator",
|
280
|
-
"Object to use to locate templates for components"
|
281
|
-
) do |components_src_dir, pages_src_dir|
|
282
|
-
Brut::FrontEnd::Templates::Locator.new(paths: [ components_src_dir, pages_src_dir ],
|
283
|
-
extension: "html.erb")
|
284
|
-
end
|
285
|
-
|
286
248
|
c.store(
|
287
249
|
"svg_locator",
|
288
|
-
"Brut::FrontEnd::
|
250
|
+
"Brut::FrontEnd::InlineSvgLocator",
|
289
251
|
"Object to use to locate SVGs"
|
290
252
|
) do |svgs_src_dir|
|
291
|
-
Brut::FrontEnd::
|
292
|
-
extension: "svg")
|
253
|
+
Brut::FrontEnd::InlineSvgLocator.new(paths: svgs_src_dir)
|
293
254
|
end
|
294
255
|
|
295
256
|
c.store(
|
296
257
|
"asset_path_resolver",
|
297
|
-
"Brut::FrontEnd::
|
258
|
+
"Brut::FrontEnd::AssetPathResolver",
|
298
259
|
"Object to use to resolve logical asset paths to actual asset paths"
|
299
260
|
) do |asset_metadata_file|
|
300
|
-
Brut::FrontEnd::
|
261
|
+
Brut::FrontEnd::AssetPathResolver.new(metadata_file: asset_metadata_file)
|
301
262
|
end
|
302
263
|
|
303
264
|
c.store(
|
data/lib/brut/framework/mcp.rb
CHANGED
@@ -186,7 +186,7 @@ class Brut::Framework::MCP
|
|
186
186
|
end
|
187
187
|
|
188
188
|
if name == :request_context
|
189
|
-
args[name] =
|
189
|
+
args[name] = Brut::FrontEnd::RequestContext.current
|
190
190
|
elsif name == :session
|
191
191
|
args[name] = Brut.container.session_class.new(rack_session: session)
|
192
192
|
elsif name == :request
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Brut::FrontEnd::AssetPathResolver
|
2
|
+
def initialize(metadata_file:)
|
3
|
+
@metadata_file = metadata_file
|
4
|
+
reload
|
5
|
+
end
|
6
|
+
|
7
|
+
def reload
|
8
|
+
@asset_metadata = Brut::FrontEnd::AssetMetadata.new(asset_metadata_file: @metadata_file)
|
9
|
+
@asset_metadata.load!
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(path)
|
13
|
+
@asset_metadata.resolve(path)
|
14
|
+
end
|
15
|
+
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
require "
|
2
|
-
require "rexml"
|
3
|
-
require_relative "template"
|
1
|
+
require "phlex"
|
4
2
|
|
5
3
|
# Components holds Brut-provided components that are of general use to any web app
|
6
4
|
module Brut::FrontEnd::Components
|
@@ -8,11 +6,13 @@ module Brut::FrontEnd::Components
|
|
8
6
|
autoload(:Input,"brut/front_end/components/input")
|
9
7
|
autoload(:Inputs,"brut/front_end/components/input")
|
10
8
|
autoload(:I18nTranslations,"brut/front_end/components/i18n_translations")
|
11
|
-
autoload(:
|
9
|
+
autoload(:TimeTag,"brut/front_end/components/time_tag")
|
12
10
|
autoload(:PageIdentifier,"brut/front_end/components/page_identifier")
|
13
11
|
autoload(:LocaleDetection,"brut/front_end/components/locale_detection")
|
14
12
|
autoload(:ConstraintViolations,"brut/front_end/components/constraint_violations")
|
15
13
|
autoload(:Traceparent,"brut/front_end/components/traceparent")
|
14
|
+
|
15
|
+
extend Phlex::Kit
|
16
16
|
end
|
17
17
|
|
18
18
|
# A Component is the top level class for managing the rendering of
|
@@ -31,64 +31,76 @@ end
|
|
31
31
|
# component's class to render the component's HTML.
|
32
32
|
#
|
33
33
|
# @see Brut::FrontEnd::Component::Helpers
|
34
|
-
class Brut::FrontEnd::Component
|
35
|
-
using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
|
34
|
+
class Brut::FrontEnd::Component < Phlex::HTML
|
36
35
|
|
36
|
+
include Brut::Framework::Errors
|
37
|
+
include Brut::I18n::ForHTML
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
register_element :brut_confirm_submit
|
40
|
+
register_element :brut_confirmation_dialog
|
41
|
+
register_element :brut_cv
|
42
|
+
register_element :brut_ajax_submit
|
43
|
+
register_element :brut_autosubmit
|
44
|
+
register_element :brut_confirm_submit
|
45
|
+
register_element :brut_confirmation_dialog
|
46
|
+
register_element :brut_cv
|
47
|
+
register_element :brut_cv_messages
|
48
|
+
register_element :brut_copy_to_clipboard
|
49
|
+
register_element :brut_form
|
50
|
+
register_element :brut_i18n_translation
|
51
|
+
register_element :brut_locale_detection
|
52
|
+
register_element :brut_message
|
53
|
+
register_element :brut_tabs
|
54
|
+
register_element :brut_tracing
|
55
|
+
|
56
|
+
def inline_svg(svg)
|
57
|
+
Brut.container.svg_locator.locate(svg).then { |svg_file|
|
58
|
+
File.read(svg_file)
|
59
|
+
}.then { |svg_content|
|
60
|
+
raw(safe(svg_content))
|
61
|
+
}
|
62
|
+
end
|
44
63
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
64
|
+
def time_tag(timestamp:nil,**component_options, &contents)
|
65
|
+
args = component_options.merge(timestamp:)
|
66
|
+
render Brut::FrontEnd::Components::TimeTag.new(**args,&contents)
|
67
|
+
end
|
49
68
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
69
|
+
def form_tag(**args, &block)
|
70
|
+
render Brut::FrontEnd::Components::FormTag.new(**args,&block)
|
53
71
|
end
|
54
72
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
attr_accessor :yielded_block
|
73
|
+
def global_component(component_klass)
|
74
|
+
render Brut::FrontEnd::RequestContext.inject(component_klass)
|
75
|
+
end
|
59
76
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
77
|
+
def constraint_violations(form:, input_name:, index: nil, message_html_attributes: {}, **html_attributes)
|
78
|
+
render(
|
79
|
+
Brut::FrontEnd::Components::ConstraintViolations.new(
|
80
|
+
form:,
|
81
|
+
input_name:,
|
82
|
+
index:,
|
83
|
+
message_html_attributes:,
|
84
|
+
**html_attributes
|
85
|
+
)
|
86
|
+
)
|
87
|
+
end
|
68
88
|
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
# @return [Brut::FrontEnd::Templates::HTMLSafeString] string containing the component's HTML.
|
81
|
-
def render
|
82
|
-
Brut.container.instrumentation.span("#{self.class} render") do |span|
|
83
|
-
span.add_prefixed_attributes("brut", type: :component, class: self.class.name)
|
84
|
-
Brut.container.component_locator.locate(self.template_name).
|
85
|
-
then { Brut::FrontEnd::Template.new(it) }.
|
86
|
-
then { it.render_template(self).html_safe! }
|
87
|
-
end
|
89
|
+
# Create an HTML input tag for the given input of a form. This is a convieniece method
|
90
|
+
# that calls {Brut::FrontEnd::Components::Inputs::TextField.for_form_input}.
|
91
|
+
def input_tag(form:, input_name:, index: nil, **html_attributes)
|
92
|
+
render(
|
93
|
+
Brut::FrontEnd::Components::Inputs::TextField.for_form_input(
|
94
|
+
form:,
|
95
|
+
input_name:,
|
96
|
+
index:,
|
97
|
+
html_attributes:)
|
98
|
+
)
|
88
99
|
end
|
89
100
|
|
90
|
-
|
91
|
-
|
101
|
+
def self.component_name = self.name
|
102
|
+
def component_name = self.class.component_name
|
103
|
+
|
92
104
|
def page_name
|
93
105
|
@page_name ||= begin
|
94
106
|
page = self.class.name.split(/::/).reduce(Module) { |accumulator,class_path_part|
|
@@ -100,192 +112,12 @@ class Brut::FrontEnd::Component
|
|
100
112
|
}
|
101
113
|
if page.ancestors.include?(Brut::FrontEnd::Page)
|
102
114
|
page.name
|
115
|
+
elsif page.respond_to?(:page_name)
|
116
|
+
page.page_name
|
103
117
|
else
|
104
118
|
raise "#{self.class} is not nested inside a page, so #page_name should not have been called"
|
105
119
|
end
|
106
120
|
end
|
107
121
|
end
|
108
122
|
|
109
|
-
# Used when an I18n string needs access to component-specific translations
|
110
|
-
def self.component_name = self.name
|
111
|
-
# (see .component_name)
|
112
|
-
def component_name = self.class.component_name
|
113
|
-
|
114
|
-
# Helper methods that subclasses can use.
|
115
|
-
# This is a separate module to distinguish the public
|
116
|
-
# interface of this class (`render`) from these helper methods
|
117
|
-
# that are useful to subclasses and their templates.
|
118
|
-
#
|
119
|
-
# This is not intended to be extracted or used outside this class!
|
120
|
-
module Helpers
|
121
|
-
|
122
|
-
# Render a component. This is the primary way in which
|
123
|
-
# view re-use happens. The component instance will be able to locate its
|
124
|
-
# HTML template and render itself. {#render} is called with variables from the `RequestContext`
|
125
|
-
# as described in {Brut::FrontEnd::RequestContext#as_method_args}
|
126
|
-
#
|
127
|
-
# @param [Brut::FrontEnd::Component|Class] component_instance instance of the component to render. If a `Class`
|
128
|
-
# is passed, it must extend {Brut::FrontEnd::Component}. It will created
|
129
|
-
# based on the logic described in {Brut::FrontEnd::RequestContext#as_constructor_args}.
|
130
|
-
# You would do this if your component needs to be injected with information
|
131
|
-
# not available to the page or component that is using it.
|
132
|
-
# @yield this block is passed to the `component_instance` via {#yielded_block=}.
|
133
|
-
#
|
134
|
-
# @return [Brut::FrontEnd::Templates::HTMLSafeString] of the rendered component.
|
135
|
-
def component(component_instance,&block)
|
136
|
-
component_name = component_instance.kind_of?(Class) ? component_instance.name : component_instance.class.name
|
137
|
-
Brut.container.instrumentation.span("component #{component_name}") do |span|
|
138
|
-
if component_instance.kind_of?(Class)
|
139
|
-
if !component_instance.ancestors.include?(Brut::FrontEnd::Component)
|
140
|
-
raise ArgumentError,"#{component_instance} is not a component and cannot be created"
|
141
|
-
end
|
142
|
-
component_instance = Thread.current.thread_variable_get(:request_context).
|
143
|
-
then { |request_context| request_context.as_constructor_args(component_instance,request_params: nil)
|
144
|
-
}.then { |constructor_args| component_instance.new(**constructor_args) }
|
145
|
-
span.add_prefixed_attributes("brut", "global_component" => true)
|
146
|
-
else
|
147
|
-
span.add_prefixed_attributes("brut", "global_component" => false)
|
148
|
-
end
|
149
|
-
if !block.nil?
|
150
|
-
component_instance.yielded_block = block
|
151
|
-
end
|
152
|
-
Thread.current.thread_variable_get(:request_context).then {
|
153
|
-
it.as_method_args(component_instance,:render,request_params: nil, form: nil)
|
154
|
-
}.then { |render_args|
|
155
|
-
component_instance.render(**render_args).html_safe!
|
156
|
-
}
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
# Inline an SVG into the page.
|
161
|
-
#
|
162
|
-
# @param [String] svg name of an SVG file, relative to where SVGs are stored.
|
163
|
-
def svg(svg)
|
164
|
-
Brut.container.svg_locator.locate(svg).then { |svg_file|
|
165
|
-
File.read(svg_file).html_safe!
|
166
|
-
}
|
167
|
-
end
|
168
|
-
|
169
|
-
# Given a public path to an asset—the value you'd use in HTML—return
|
170
|
-
# the same value, but with any content hashes that are part of the filename.
|
171
|
-
def asset_path(path) = Brut.container.asset_path_resolver.resolve(path)
|
172
|
-
|
173
|
-
# (see Brut::FrontEnd::Components::FormTag)
|
174
|
-
def form_tag(route_params: {}, **html_attributes,&contents)
|
175
|
-
component(Brut::FrontEnd::Components::FormTag.new(route_params:, **html_attributes,&contents))
|
176
|
-
end
|
177
|
-
|
178
|
-
# Creates a {Brut::FrontEnd::Components::Time}.
|
179
|
-
#
|
180
|
-
# @param timestamp [Time] the timestamp to format/render. Mutually exclusive with `date`.
|
181
|
-
# @param date [Date] the date to format/render. Mutually exclusive with `timestamp`.
|
182
|
-
# @param component_options [Hash] keyword arguments to pass to {Brut::FrontEnd::Components::Time#initialize}
|
183
|
-
# @yield See {Brut::FrontEnd::Components::Time#initialize}
|
184
|
-
def time_tag(timestamp:nil,date:nil, **component_options, &contents)
|
185
|
-
args = component_options.merge(timestamp:,date:)
|
186
|
-
component(Brut::FrontEnd::Components::Time.new(**args,&contents))
|
187
|
-
end
|
188
|
-
|
189
|
-
# Render the {Brut::FrontEnd::Components::ConstraintViolations} component for the given form's input.
|
190
|
-
def constraint_violations(form:, input_name:, index: nil, message_html_attributes: {}, **html_attributes)
|
191
|
-
component(
|
192
|
-
Brut::FrontEnd::Components::ConstraintViolations.new(
|
193
|
-
form:,
|
194
|
-
input_name:,
|
195
|
-
index:,
|
196
|
-
message_html_attributes:,
|
197
|
-
**html_attributes
|
198
|
-
)
|
199
|
-
)
|
200
|
-
end
|
201
|
-
|
202
|
-
# Create an HTML input tag for the given input of a form. This is a convieniece method
|
203
|
-
# that calls {Brut::FrontEnd::Components::Inputs::TextField.for_form_input}.
|
204
|
-
def input_tag(form:, input_name:, index: nil, **html_attributes)
|
205
|
-
component(Brut::FrontEnd::Components::Inputs::TextField.for_form_input(form:,input_name:,index:,html_attributes:))
|
206
|
-
end
|
207
|
-
|
208
|
-
# Indicates a given string is safe to render directly as HTML. No escaping will happen.
|
209
|
-
#
|
210
|
-
# @param [String] string a string that should be marked as HTML safe
|
211
|
-
def html_safe!(string)
|
212
|
-
string.html_safe!
|
213
|
-
end
|
214
|
-
|
215
|
-
# @!visibility private
|
216
|
-
VOID_ELEMENTS = [
|
217
|
-
:area,
|
218
|
-
:base,
|
219
|
-
:br,
|
220
|
-
:col,
|
221
|
-
:embed,
|
222
|
-
:hr,
|
223
|
-
:img,
|
224
|
-
:input,
|
225
|
-
:link,
|
226
|
-
:meta,
|
227
|
-
:source,
|
228
|
-
:track,
|
229
|
-
:wbr,
|
230
|
-
]
|
231
|
-
|
232
|
-
# Generate an HTML element safely in code. This is useful if you don't want to create
|
233
|
-
# a separate ERB file, but still want to create a component.
|
234
|
-
#
|
235
|
-
# @param [String|Symbol] tag_name the name of the HTML tag to create.
|
236
|
-
# @param [Hash] html_attributes all the HTML attributes you wish to include in the element that is generated. Values that
|
237
|
-
# are `true` will be included without a value, and values that are `false` will be omitted.
|
238
|
-
# @yield Called to get any contents that should be put into this tag. Void elements as defined by W3C may not have a block.
|
239
|
-
#
|
240
|
-
# @example Void element
|
241
|
-
#
|
242
|
-
# html_tag(:img, src: "trellick.png") # => <img src="trellic.png">
|
243
|
-
#
|
244
|
-
# @example Nested elements
|
245
|
-
#
|
246
|
-
# html_tag(:nav, class: "flex items-center") do
|
247
|
-
# html_tag(:a, href="/") { "Home" } +
|
248
|
-
# html_tag(:a, href="/about") { "About" } +
|
249
|
-
# html_tag(:a, href="/contact") { "Contact" }
|
250
|
-
# end
|
251
|
-
def html_tag(tag_name, **html_attributes, &block)
|
252
|
-
tag_name = tag_name.to_s.downcase.to_sym
|
253
|
-
attributes_string = html_attributes.map { |key,value|
|
254
|
-
[
|
255
|
-
key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
|
256
|
-
value
|
257
|
-
]
|
258
|
-
}.select { |key,value|
|
259
|
-
!value.nil?
|
260
|
-
}.map { |key,value|
|
261
|
-
if value == true
|
262
|
-
key
|
263
|
-
elsif value == false
|
264
|
-
""
|
265
|
-
else
|
266
|
-
REXML::Attribute.new(key,value).to_string
|
267
|
-
end
|
268
|
-
}.join(" ")
|
269
|
-
contents = (block.nil? ? nil : block.()).to_s
|
270
|
-
if VOID_ELEMENTS.include?(tag_name)
|
271
|
-
if !contents.empty?
|
272
|
-
raise ArgumentError,"#{tag_name} may not have child nodes"
|
273
|
-
end
|
274
|
-
html_safe!(%{<#{tag_name} #{attributes_string}>})
|
275
|
-
else
|
276
|
-
html_safe!(%{<#{tag_name} #{attributes_string}>#{contents}</#{tag_name}>})
|
277
|
-
end
|
278
|
-
end
|
279
|
-
end
|
280
|
-
include Helpers
|
281
|
-
include Brut::I18n::ForHTML
|
282
|
-
|
283
|
-
private
|
284
|
-
|
285
|
-
def binding_scope = binding
|
286
|
-
|
287
|
-
# Determines the canonical name/location of the template used for this
|
288
|
-
# component. It does this base do the class name. CameCase is converted
|
289
|
-
# to snake_case.
|
290
|
-
def template_name = RichString.new(self.class.name).underscorized.to_s.gsub(/^components\//,"")
|
291
123
|
end
|