brut 0.0.13 → 0.0.21

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 (82) 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/seed_data.rb +19 -2
  5. data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
  6. data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
  7. data/lib/brut/back_end/sidekiq.rb +2 -1
  8. data/lib/brut/back_end/validator.rb +5 -1
  9. data/lib/brut/back_end.rb +9 -0
  10. data/lib/brut/cli/apps/scaffold.rb +16 -24
  11. data/lib/brut/cli.rb +4 -3
  12. data/lib/brut/factory_bot.rb +0 -5
  13. data/lib/brut/framework/app.rb +70 -5
  14. data/lib/brut/framework/config.rb +9 -46
  15. data/lib/brut/framework/container.rb +3 -2
  16. data/lib/brut/framework/errors.rb +12 -4
  17. data/lib/brut/framework/mcp.rb +63 -2
  18. data/lib/brut/framework/project_environment.rb +6 -2
  19. data/lib/brut/framework.rb +1 -1
  20. data/lib/brut/front_end/asset_path_resolver.rb +15 -0
  21. data/lib/brut/front_end/component.rb +101 -246
  22. data/lib/brut/front_end/components/constraint_violations.rb +10 -10
  23. data/lib/brut/front_end/components/form_tag.rb +17 -29
  24. data/lib/brut/front_end/components/i18n_translations.rb +12 -13
  25. data/lib/brut/front_end/components/input.rb +0 -1
  26. data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
  27. data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
  28. data/lib/brut/front_end/components/inputs/select.rb +13 -20
  29. data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
  30. data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
  31. data/lib/brut/front_end/components/locale_detection.rb +2 -2
  32. data/lib/brut/front_end/components/page_identifier.rb +3 -5
  33. data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
  34. data/lib/brut/front_end/components/traceparent.rb +5 -6
  35. data/lib/brut/front_end/http_method.rb +4 -0
  36. data/lib/brut/front_end/inline_svg_locator.rb +21 -0
  37. data/lib/brut/front_end/layout.rb +19 -0
  38. data/lib/brut/front_end/page.rb +52 -40
  39. data/lib/brut/front_end/request_context.rb +13 -0
  40. data/lib/brut/front_end/routing.rb +8 -3
  41. data/lib/brut/front_end.rb +32 -0
  42. data/lib/brut/i18n/base_methods.rb +51 -11
  43. data/lib/brut/i18n/for_back_end.rb +8 -0
  44. data/lib/brut/i18n/for_cli.rb +5 -1
  45. data/lib/brut/i18n/for_html.rb +9 -1
  46. data/lib/brut/i18n/http_accept_language.rb +47 -0
  47. data/lib/brut/i18n.rb +1 -0
  48. data/lib/brut/instrumentation/open_telemetry.rb +25 -0
  49. data/lib/brut/instrumentation.rb +3 -5
  50. data/lib/brut/sinatra_helpers.rb +13 -7
  51. data/lib/brut/spec_support/component_support.rb +27 -13
  52. data/lib/brut/spec_support/e2e_support.rb +4 -0
  53. data/lib/brut/spec_support/general_support.rb +3 -0
  54. data/lib/brut/spec_support/handler_support.rb +6 -1
  55. data/lib/brut/spec_support/matcher.rb +1 -0
  56. data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
  57. data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
  58. data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
  59. data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
  60. data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
  61. data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
  62. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
  63. data/lib/brut/spec_support/rspec_setup.rb +1 -0
  64. data/lib/brut/spec_support.rb +5 -4
  65. data/lib/brut/version.rb +1 -1
  66. data/lib/brut.rb +7 -50
  67. metadata +14 -49
  68. data/doc-src/architecture.md +0 -102
  69. data/doc-src/assets.md +0 -98
  70. data/doc-src/forms.md +0 -214
  71. data/doc-src/handlers.md +0 -83
  72. data/doc-src/javascript.md +0 -265
  73. data/doc-src/keyword-injection.md +0 -183
  74. data/doc-src/pages.md +0 -210
  75. data/doc-src/route-hooks.md +0 -59
  76. data/lib/brut/front_end/template.rb +0 -47
  77. data/lib/brut/front_end/templates/block_filter.rb +0 -61
  78. data/lib/brut/front_end/templates/erb_engine.rb +0 -26
  79. data/lib/brut/front_end/templates/erb_parser.rb +0 -84
  80. data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
  81. data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
  82. data/lib/brut/front_end/templates/locator.rb +0 -60
@@ -1,5 +1,7 @@
1
- # Manages the interpretation of dev/test/prod. The canonical instance is available via `Brut.container.project_env`. Generally, you
2
- # should avoid basing logic on this, or at least contain the conditional behavior to the configuration values. But, you do you.
1
+ # Manages the interpretation of dev/test/prod. The canonical instance is available
2
+ # via `Brut.container.project_env`. Generally, you
3
+ # should avoid basing logic on this, or at least contain the conditional behavior
4
+ # to the configuration values. But, you do you.
3
5
  class Brut::Framework::ProjectEnvironment
4
6
  # Create the project environment based on the string
5
7
  # @param [String] string_value value from e.g. `ENV["RACK_ENV"]` to use to set the environment
@@ -21,6 +23,8 @@ class Brut::Framework::ProjectEnvironment
21
23
  # @return [true|false] true is this is production
22
24
  def production? = @value == "production"
23
25
 
26
+ def staging? = raise "Staging is a lie, please consider feature flags or literally any other way to manage in-development features of your app. I promise you, you will regret ever having to do anything with a staging server"
27
+
24
28
  # @return [String] the string value (which should be suitable for the constructor)
25
29
  def to_s = @value
26
30
  end
@@ -1,5 +1,5 @@
1
1
  module Brut
2
- # The Framework module holds a lot of Brut's internals, or classes that cut across the back end and front end.
2
+ # Namespace for Brut's internals as well as classes that aren't strictly front or back end.
3
3
  module Framework
4
4
  autoload(:App,"brut/framework/app")
5
5
  autoload(:Config,"brut/framework/config")
@@ -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,87 +6,124 @@ 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
19
- # content. A component is essentially an ERB template and a class whose
20
- # instance servces as it's binding. It is very similar to a View Component, though
21
- # not quite as fancy.
19
+ # content. It is a Phlex component with additional features.
20
+ # Components are the primary mechanism for managing view complexity and managing
21
+ # markup re-use in Brut.
22
+ #
23
+ # To create a component, subclass this class (or, more likely, your app's `AppComponent`) and
24
+ # provide an initializer that accepts keyword arguments. The names of these arguments will be used to locate the
25
+ # values that Brut will pass in when creating your component object.
22
26
  #
23
- # When subclassing this to create a component, your initializer's signature will determine what data
24
- # is required for your component to work. It can be anything, just keep in mind that any page or component
25
- # that uses your component must be able to provide those values.
27
+ # Consult Brut's documentation on keyword injection to know what values you may use and how values are located.
26
28
  #
27
- # If your component does not override {#render} (which, generally, it won't), an ERB file is expected to exist alongside it in the
28
- # app. For example, if you have a component named `Auth::LoginButtonComponent`, it would expected to be in
29
- # `app/src/front_end/components/auth/login_button_component.rb`. Thus, Brut will also expect
30
- # `app/src/front_end/components/auth/login_button_component.html.erb` to exist as well. That ERB file is used with an instance of your
31
- # component's class to render the component's HTML.
29
+ # Becuase this is a Phlex component, you must implement `view_template` and make calls to Phlex's API to create
30
+ # the markup for your component.
32
31
  #
33
- # @see Brut::FrontEnd::Component::Helpers
34
- class Brut::FrontEnd::Component
35
- using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
32
+ class Brut::FrontEnd::Component < Phlex::HTML
36
33
 
34
+ include Brut::Framework::Errors
35
+ include Brut::I18n::ForHTML
37
36
 
38
- # @!visibility private
39
- class AssetPathResolver
40
- def initialize(metadata_file:)
41
- @metadata_file = metadata_file
42
- reload
43
- end
37
+ register_element :brut_confirm_submit
38
+ register_element :brut_confirmation_dialog
39
+ register_element :brut_cv
40
+ register_element :brut_ajax_submit
41
+ register_element :brut_autosubmit
42
+ register_element :brut_confirm_submit
43
+ register_element :brut_confirmation_dialog
44
+ register_element :brut_cv
45
+ register_element :brut_cv_messages
46
+ register_element :brut_copy_to_clipboard
47
+ register_element :brut_form
48
+ register_element :brut_i18n_translation
49
+ register_element :brut_locale_detection
50
+ register_element :brut_message
51
+ register_element :brut_tabs
52
+ register_element :brut_tracing
53
+
54
+ # Inline an SVG that is part of your app.
55
+ #
56
+ # @param [String] svg path to the SVG file, relative to where SVGs are
57
+ # stored, which is `app/src/front_end/svgs` or where `Brut.container.svg_locator` is
58
+ # looking
59
+ #
60
+ # @see Brut::FrontEnd::InlineSvgLocator
61
+ def inline_svg(svg)
62
+ Brut.container.svg_locator.locate(svg).then { |svg_file|
63
+ File.read(svg_file)
64
+ }.then { |svg_content|
65
+ raw(safe(svg_content))
66
+ }
67
+ end
44
68
 
45
- def reload
46
- @asset_metadata = Brut::FrontEnd::AssetMetadata.new(asset_metadata_file: @metadata_file)
47
- @asset_metadata.load!
48
- end
69
+ # Include a {Brut::FrontEnd::Components::TimeTag} in your markup.
70
+ def time_tag(timestamp:nil,**component_options, &contents)
71
+ args = component_options.merge(timestamp:)
72
+ render Brut::FrontEnd::Components::TimeTag.new(**args,&contents)
73
+ end
49
74
 
50
- def resolve(path)
51
- @asset_metadata.resolve(path)
52
- end
75
+ # Include a {Brut::FrontEnd::Components::FormTag} in your markup.
76
+ def form_tag(**args, &block)
77
+ render Brut::FrontEnd::Components::FormTag.new(**args,&block)
53
78
  end
54
79
 
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
80
+ # Include a component in your markup that you would like Brut to instantiate.
81
+ # This will use keyword injection to create the component, which means that if the component
82
+ # doesn't require any data from this component, you do not need to pass through those values.
83
+ # For example, you may have a component that renders the flash message. To avoid requiring your component to
84
+ # be passed the flash, a global component can be injected with it from Brut.
85
+ def global_component(component_klass)
86
+ render Brut::FrontEnd::RequestContext.inject(component_klass)
87
+ end
59
88
 
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
89
+ # include a {Brut::FrontEnd::Components::ConstraintViolations} in your markup.
90
+ def constraint_violations(form:, input_name:, index: nil, message_html_attributes: {}, **html_attributes)
91
+ render(
92
+ Brut::FrontEnd::Components::ConstraintViolations.new(
93
+ form:,
94
+ input_name:,
95
+ index:,
96
+ message_html_attributes:,
97
+ **html_attributes
98
+ )
99
+ )
100
+ end
68
101
 
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
102
+ # Create an HTML input tag for the given input of a form. This is a convieniece method
103
+ # that calls {Brut::FrontEnd::Components::Inputs::TextField.for_form_input}.
104
+ def input_tag(form:, input_name:, index: nil, **html_attributes)
105
+ render(
106
+ Brut::FrontEnd::Components::Inputs::TextField.for_form_input(
107
+ form:,
108
+ input_name:,
109
+ index:,
110
+ html_attributes:)
111
+ )
88
112
  end
89
113
 
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.
114
+ # The name of this component, used for debugging and other purposes. Do not
115
+ # override this.
116
+ def self.component_name = self.name
117
+
118
+ # Calls {.component_name} as a convienience. Do not override this.
119
+ def component_name = self.class.component_name
120
+
121
+ # For page components (components that are private/nested to a page), this returns
122
+ # the name of the page in which they are nested. This is mostly useful for
123
+ # locating page-specific I18n translations.
124
+ #
125
+ # @raise If this component is not nested inside a page
126
+ # @see Brut::I18n::BaseMethods#t
92
127
  def page_name
93
128
  @page_name ||= begin
94
129
  page = self.class.name.split(/::/).reduce(Module) { |accumulator,class_path_part|
@@ -100,192 +135,12 @@ class Brut::FrontEnd::Component
100
135
  }
101
136
  if page.ancestors.include?(Brut::FrontEnd::Page)
102
137
  page.name
138
+ elsif page.respond_to?(:page_name)
139
+ page.page_name
103
140
  else
104
141
  raise "#{self.class} is not nested inside a page, so #page_name should not have been called"
105
142
  end
106
143
  end
107
144
  end
108
145
 
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
146
  end
@@ -17,7 +17,7 @@
17
17
  # Note that if you are using `<brut-form>` then `<brut-cv>` elements will be inserted into the `<brut-cv-messages>` element, however
18
18
  # they will not have the `server-side` attribute.
19
19
  #
20
- # You will most commonly use this component via {Brut::FrontEnd::Component::Helpers#constraint_violations}.
20
+ # You will most commonly use this component via {Brut::FrontEnd::Component#constraint_violations}.
21
21
  class Brut::FrontEnd::Components::ConstraintViolations < Brut::FrontEnd::Component
22
22
  # Create a new ConstraintViolations component
23
23
  #
@@ -31,27 +31,27 @@ class Brut::FrontEnd::Components::ConstraintViolations < Brut::FrontEnd::Compone
31
31
  @input_name = input_name
32
32
  @array = !index.nil?
33
33
  @index = index || 0
34
- @html_attributes = html_attributes
35
- @message_html_attributes = message_html_attributes
34
+ @html_attributes = html_attributes.map {|name,value| [ name.to_sym, value ] }.to_h
35
+ @message_html_attributes = message_html_attributes.map {|name,value| [ name.to_sym, value ] }.to_h
36
36
  end
37
37
 
38
- def render
38
+ def view_template
39
39
  html_attributes = {
40
- "input-name": @array ? "#{@input_name}[]" : @input_name
40
+ "input-name": @array ? "#{@input_name}[]" : @input_name.to_s,
41
41
  }.merge(@html_attributes)
42
42
 
43
43
  message_html_attributes = {
44
44
  "server-side": true,
45
45
  }.merge(@message_html_attributes)
46
46
 
47
- html_tag("brut-cv-messages", **html_attributes) do
47
+ brut_cv_messages(**html_attributes) do
48
48
  @form.input(@input_name, index: @index).validity_state.select { |constraint|
49
49
  !constraint.client_side?
50
- }.map { |constraint|
51
- html_tag("brut-cv",**message_html_attributes) do
52
- t("cv.be.#{constraint}", **constraint.context).capitalize
50
+ }.each do |constraint|
51
+ brut_cv(**message_html_attributes) do
52
+ t("cv.be.#{constraint}", **constraint.context)
53
53
  end
54
- }.join("\n")
54
+ end
55
55
  end
56
56
  end
57
57
  end
@@ -1,5 +1,4 @@
1
- require "rexml"
2
- # Represents a `<form>` HTML element that includes a CSRF token as needed. You likely want to use this class via the {Brut::FrontEnd::Component::Helpers#form_tag} method.
1
+ # Represents a `<form>` HTML element that includes a CSRF token as needed. You likely want to use this class via the {Brut::FrontEnd::Component#form_tag} method.
3
2
  class Brut::FrontEnd::Components::FormTag < Brut::FrontEnd::Component
4
3
  # Creates the form surrounding the contents of the block yielded to it. If the form's action is a POST, it will include a CSRF token.
5
4
  # If the form's action is GET, it will not.
@@ -22,7 +21,7 @@ class Brut::FrontEnd::Components::FormTag < Brut::FrontEnd::Component
22
21
  # @option html_attributes [Class|Brut::FrontEnd::Form] :for the form object or class representing this HTML form *or* the class of a handler the form should submit to. If you pass this, you may not pass the HTML attributes `:action` or `:method`. Both will be derived from this object.
23
22
  # @option html_attributes [String] «any-other-key» attributes to set on the `<form>` tag
24
23
  # @yield No parameters given. This is expected to return additional markup to appear inside the `<form>` element.
25
- def initialize(route_params: {}, **html_attributes,&contents)
24
+ def initialize(route_params: {}, **html_attributes)
26
25
  form_class = html_attributes.delete(:for) # Cannot be a keyword arg, since for is a reserved word
27
26
  if !form_class.nil?
28
27
  if form_class.kind_of?(Brut::FrontEnd::Form)
@@ -49,34 +48,23 @@ class Brut::FrontEnd::Components::FormTag < Brut::FrontEnd::Component
49
48
 
50
49
  @include_csrf_token = http_method.post?
51
50
  @csrf_token_omit_reasoning = http_method.get? ? "because this form's action is a GET" : nil
52
- @attributes = html_attributes
53
- @contents = contents
51
+ @attributes = html_attributes.merge(method: http_method)
54
52
  end
55
53
 
56
- # @!visibility private
57
- def render
58
- attribute_string = @attributes.map { |key,value|
59
- key = key.to_s
60
- if value == true
61
- key
62
- elsif value == false
63
- ""
64
- else
65
- REXML::Attribute.new(key,value).to_string
54
+ def view_template
55
+ form(**@attributes) do
56
+ if @include_csrf_token
57
+ render Brut::FrontEnd::RequestContext.inject(Brut::FrontEnd::Components::Inputs::CsrfToken)
58
+ elsif Brut.container.project_env.development?
59
+ comment do
60
+ "CSRF Token omitted #{@csrf_token_omit_reasoning} (this message only appears in development)"
61
+ end
66
62
  end
67
- }.join(" ")
68
- csrf_token_component = if @include_csrf_token
69
- component(Brut::FrontEnd::Components::Inputs::CsrfToken)
70
- elsif Brut.container.project_env.development?
71
- html_safe!("<!-- CSRF Token omitted #{@csrf_token_omit_reasoning} (this message only appears in development) -->")
72
- else
73
- ""
74
- end
75
- %{
76
- <form #{attribute_string}>
77
- #{ csrf_token_component }
78
- #{ @contents.() }
79
- </form>
80
- }
63
+ if block_given?
64
+ yield
65
+ end
66
+ end
81
67
  end
68
+
69
+
82
70
  end
@@ -1,5 +1,3 @@
1
- require "rexml"
2
-
3
1
  # Produces `<brut-i18n-translation>` entries for the given values. This is used for client-side constraint violation messaging with
4
2
  # JavaScript. The `<brut-constraint-violation-message>` tag uses these keys to produce messages on the client.
5
3
  #
@@ -39,13 +37,13 @@ class Brut::FrontEnd::Components::I18nTranslations < Brut::FrontEnd::Component
39
37
  end
40
38
 
41
39
  # @!visibility private
42
- def render
40
+ def view_template
43
41
  values = ::I18n.t(@i18n_key_root)
44
42
  if values.kind_of?(String)
45
43
  values = { "" => values }
46
44
  end
47
45
 
48
- values.map { |key,value|
46
+ values.each do |key,value|
49
47
  if !value.kind_of?(String)
50
48
  raise "Key #{key} under #{@i18n_key_root} maps to a #{value.class} instead of a String. For #{self.class} to work, the value must be a String"
51
49
  end
@@ -54,16 +52,17 @@ class Brut::FrontEnd::Components::I18nTranslations < Brut::FrontEnd::Component
54
52
  else
55
53
  "#{@i18n_key_root}.#{key}"
56
54
  end
57
- attributes = [
58
- REXML::Attribute.new("key",i18n_key),
59
- REXML::Attribute.new("value",value.to_s),
60
- ]
55
+ attributes = {
56
+ key: i18n_key,
57
+ value: value.to_s,
58
+ }
61
59
  if !Brut.container.project_env.production?
62
- attributes << REXML::Attribute.new("show-warnings",true)
63
- attributes << REXML::Attribute.new("id","brut-18n-#{key}")
60
+ attributes[:show_warnings] = true
61
+ attributes[:id] = "brut-18n-#{key}"
64
62
  end
65
- attribute_string = attributes.map(&:to_string).join(" ")
66
- %{<brut-i18n-translation #{attribute_string}></brut-i18n-translation>}
67
- }.join("\n")
63
+
64
+ brut_i18n_translation(**attributes)
65
+
66
+ end
68
67
  end
69
68
  end
@@ -1,4 +1,3 @@
1
- require "rexml"
2
1
  module Brut::FrontEnd::Components
3
2
 
4
3
  # Holds components designed to render HTML `<input>` and other form components.
@@ -1,10 +1,10 @@
1
1
  # Renders a hidden field for a form that contains the current CSRF token. You only need
2
- # to use this directly if you are building a form without {Brut::FrontEnd::Component::Helpers#form_tag}.
2
+ # to use this directly if you are building a form without {Brut::FrontEnd::Component#form_tag}.
3
3
  class Brut::FrontEnd::Components::Inputs::CsrfToken < Brut::FrontEnd::Components::Input
4
4
  def initialize(csrf_token:)
5
5
  @csrf_token = csrf_token
6
6
  end
7
- def render
8
- html_tag(:input, type: "hidden", name: "authenticity_token", value: @csrf_token)
7
+ def view_template
8
+ input(type: "hidden", name: "authenticity_token", value: @csrf_token)
9
9
  end
10
10
  end
@@ -14,18 +14,18 @@ class Brut::FrontEnd::Components::Inputs::RadioButton < Brut::FrontEnd::Componen
14
14
  # @param [Hash] html_attributes any additional HTML attributes to include on the `<input>` element.
15
15
  def self.for_form_input(form:, input_name:, value:, html_attributes: {})
16
16
  default_html_attributes = {}
17
- html_attributes = html_attributes.map { |key,value| [ key.to_s, value ] }.to_h
17
+ html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
18
18
  input = form.input(input_name)
19
19
 
20
- default_html_attributes["required"] = input.required
21
- default_html_attributes["type"] = "radio"
22
- default_html_attributes["name"] = input.name
23
- default_html_attributes["value"] = value
20
+ default_html_attributes[:required] = input.required
21
+ default_html_attributes[:type] = "radio"
22
+ default_html_attributes[:name] = input.name
23
+ default_html_attributes[:value] = value
24
24
 
25
25
  selected_value = input.value
26
26
 
27
27
  if selected_value == value
28
- default_html_attributes["checked"] = true
28
+ default_html_attributes[:checked] = true
29
29
  end
30
30
 
31
31
  if !form.new? && !input.valid?