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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -6
  3. data/brut.gemspec +1 -3
  4. data/lib/brut/back_end.rb +7 -0
  5. data/lib/brut/cli/apps/scaffold.rb +16 -24
  6. data/lib/brut/framework/config.rb +4 -43
  7. data/lib/brut/framework/mcp.rb +1 -1
  8. data/lib/brut/front_end/asset_path_resolver.rb +15 -0
  9. data/lib/brut/front_end/component.rb +66 -234
  10. data/lib/brut/front_end/components/constraint_violations.rb +9 -9
  11. data/lib/brut/front_end/components/form_tag.rb +16 -28
  12. data/lib/brut/front_end/components/i18n_translations.rb +12 -13
  13. data/lib/brut/front_end/components/input.rb +0 -1
  14. data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -2
  15. data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
  16. data/lib/brut/front_end/components/inputs/select.rb +13 -20
  17. data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
  18. data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
  19. data/lib/brut/front_end/components/locale_detection.rb +2 -2
  20. data/lib/brut/front_end/components/page_identifier.rb +3 -5
  21. data/lib/brut/front_end/components/{time.rb → time_tag.rb} +13 -10
  22. data/lib/brut/front_end/components/traceparent.rb +5 -6
  23. data/lib/brut/front_end/http_method.rb +4 -0
  24. data/lib/brut/front_end/inline_svg_locator.rb +21 -0
  25. data/lib/brut/front_end/layout.rb +3 -0
  26. data/lib/brut/front_end/page.rb +16 -29
  27. data/lib/brut/front_end/request_context.rb +13 -0
  28. data/lib/brut/front_end/routing.rb +3 -2
  29. data/lib/brut/front_end.rb +41 -0
  30. data/lib/brut/i18n/base_methods.rb +14 -8
  31. data/lib/brut/i18n/for_back_end.rb +5 -0
  32. data/lib/brut/i18n/for_cli.rb +2 -1
  33. data/lib/brut/i18n/for_html.rb +9 -1
  34. data/lib/brut/i18n.rb +1 -0
  35. data/lib/brut/sinatra_helpers.rb +12 -7
  36. data/lib/brut/spec_support/component_support.rb +9 -9
  37. data/lib/brut/spec_support/e2e_support.rb +4 -0
  38. data/lib/brut/spec_support/matchers/have_i18n_string.rb +5 -0
  39. data/lib/brut/spec_support/rspec_setup.rb +1 -0
  40. data/lib/brut/spec_support.rb +4 -3
  41. data/lib/brut/version.rb +1 -1
  42. data/lib/brut.rb +2 -46
  43. metadata +13 -41
  44. data/lib/brut/front_end/template.rb +0 -47
  45. data/lib/brut/front_end/templates/block_filter.rb +0 -61
  46. data/lib/brut/front_end/templates/erb_engine.rb +0 -26
  47. data/lib/brut/front_end/templates/erb_parser.rb +0 -84
  48. data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
  49. data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
  50. 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: 8e8b01889cc06839d9cca47e021c8b781e6f21417b08ba0658d55697285bb061
4
- data.tar.gz: 4d1bf20dac16e8e6322402aa364f886235140028e53f8aa313ddc6d3b2de81c6
3
+ metadata.gz: 8a6e2324291c72fa3b3012b38b604104ce6d00af875326c20df00cfc42d16a9c
4
+ data.tar.gz: c16981e60940c676e21590f9a3db5845c8cc77ee2c7dd86eba9cfb8777ac7a32
5
5
  SHA512:
6
- metadata.gz: 77c38b80c31452da7db8ef80ce2532aa1d3193257193d70625135d23af01695cc5f0c3f46568988bcf0c02849af8b8f9f3f166f9065445efd90a160c1e3206c0
7
- data.tar.gz: e36fa4f081d48518cf941d0e0323cde04a6370082e0a594da73221f65c1f319d29df29c7cbd5663f30b2da2776fa8ec24295a16cc55b41b94c6f1096590282f7
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.13)
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, template, and associated test"
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 #{source_path.relative_path_from(Brut.container.project_root)}"
266
- out.puts "Component HTML template is in #{html_source_path.relative_path_from(Brut.container.project_root)}"
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, template, and associated test"
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 #{page_source_path.relative_path_from(Brut.container.project_root)}"
397
- out.puts "Page HTML template is in #{template_source_path.relative_path_from(Brut.container.project_root)}"
398
- out.puts "Page test is in #{page_spec_path.relative_path_from(Brut.container.project_root)}"
399
- out.puts "Added title to #{app_translations.relative_path_from(Brut.container.project_root)}"
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::Templates::Locator",
250
+ "Brut::FrontEnd::InlineSvgLocator",
289
251
  "Object to use to locate SVGs"
290
252
  ) do |svgs_src_dir|
291
- Brut::FrontEnd::Templates::Locator.new(paths: svgs_src_dir,
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::Component::AssetPathResolver",
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::Component::AssetPathResolver.new(metadata_file: asset_metadata_file)
261
+ Brut::FrontEnd::AssetPathResolver.new(metadata_file: asset_metadata_file)
301
262
  end
302
263
 
303
264
  c.store(
@@ -186,7 +186,7 @@ class Brut::Framework::MCP
186
186
  end
187
187
 
188
188
  if name == :request_context
189
- args[name] = Thread.current.thread_variable_get(:request_context)
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 "json"
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(:Time,"brut/front_end/components/time")
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
- # @!visibility private
39
- class AssetPathResolver
40
- def initialize(metadata_file:)
41
- @metadata_file = metadata_file
42
- reload
43
- end
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
- def reload
46
- @asset_metadata = Brut::FrontEnd::AssetMetadata.new(asset_metadata_file: @metadata_file)
47
- @asset_metadata.load!
48
- end
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
- def resolve(path)
51
- @asset_metadata.resolve(path)
52
- end
69
+ def form_tag(**args, &block)
70
+ render Brut::FrontEnd::Components::FormTag.new(**args,&block)
53
71
  end
54
72
 
55
- # Allows helpers that create components to pass the block they were given to the component.
56
- # This can be read for the purposes of nested components passing a yielded block to an inner
57
- # component
58
- attr_accessor :yielded_block
73
+ def global_component(component_klass)
74
+ render Brut::FrontEnd::RequestContext.inject(component_klass)
75
+ end
59
76
 
60
- # Intended to be called by subclasses to render the yielded block wherever it makes sense in their markup.
61
- def render_yielded_block
62
- if @yielded_block
63
- @yielded_block.().html_safe!
64
- else
65
- raise Brut::Framework::Errors::Bug, "No block was yielded to #{self.class.name}"
66
- end
67
- end
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
- # The core method of a component. This is expected to return
70
- # a string to be sent as a response to an HTTP request. Generally, you should not call this method
71
- # as it is intended to be called from {Brut::FrontEnd::Component::Helpers#component}.
72
- #
73
- # This implementation uses the associated template for the component
74
- # and sends it through ERB using this component as
75
- # the binding.
76
- #
77
- # You may override this method to provide your own HTML for the component. In doing so, you can add
78
- # keyword args for data from the `RequestContext` you wish to receive. See {Brut::FrontEnd::RequestContext#as_method_args}.
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
- # For components that are private to a page, this returns the name of the page they are a part of.
91
- # This is used to allow a component to render a page's I18n strings.
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