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.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -6
- data/brut.gemspec +1 -3
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +9 -0
- data/lib/brut/cli/apps/scaffold.rb +16 -24
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +9 -46
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +63 -2
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/asset_path_resolver.rb +15 -0
- data/lib/brut/front_end/component.rb +101 -246
- data/lib/brut/front_end/components/constraint_violations.rb +10 -10
- data/lib/brut/front_end/components/form_tag.rb +17 -29
- 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 +3 -3
- 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} +14 -11
- 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 +19 -0
- data/lib/brut/front_end/page.rb +52 -40
- data/lib/brut/front_end/request_context.rb +13 -0
- data/lib/brut/front_end/routing.rb +8 -3
- data/lib/brut/front_end.rb +32 -0
- data/lib/brut/i18n/base_methods.rb +51 -11
- data/lib/brut/i18n/for_back_end.rb +8 -0
- data/lib/brut/i18n/for_cli.rb +5 -1
- data/lib/brut/i18n/for_html.rb +9 -1
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/open_telemetry.rb +25 -0
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +13 -7
- data/lib/brut/spec_support/component_support.rb +27 -13
- data/lib/brut/spec_support/e2e_support.rb +4 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/spec_support.rb +5 -4
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +7 -50
- metadata +14 -49
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/keyword-injection.md +0 -183
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- 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
@@ -1,5 +1,7 @@
|
|
1
|
-
# Manages the interpretation of dev/test/prod. The canonical instance is available
|
2
|
-
#
|
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
|
data/lib/brut/framework.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Brut
|
2
|
-
#
|
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 "
|
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(:
|
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.
|
20
|
-
#
|
21
|
-
#
|
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
|
-
#
|
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
|
-
#
|
28
|
-
#
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
#
|
56
|
-
# This
|
57
|
-
# component
|
58
|
-
|
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
|
-
#
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
#
|
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
|
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
|
-
#
|
91
|
-
#
|
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
|
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
|
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
|
-
|
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
|
-
}.
|
51
|
-
|
52
|
-
t("cv.be.#{constraint}", **constraint.context)
|
50
|
+
}.each do |constraint|
|
51
|
+
brut_cv(**message_html_attributes) do
|
52
|
+
t("cv.be.#{constraint}", **constraint.context)
|
53
53
|
end
|
54
|
-
|
54
|
+
end
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -1,5 +1,4 @@
|
|
1
|
-
|
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
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
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.
|
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
|
-
|
59
|
-
|
60
|
-
|
55
|
+
attributes = {
|
56
|
+
key: i18n_key,
|
57
|
+
value: value.to_s,
|
58
|
+
}
|
61
59
|
if !Brut.container.project_env.production?
|
62
|
-
attributes
|
63
|
-
attributes
|
60
|
+
attributes[:show_warnings] = true
|
61
|
+
attributes[:id] = "brut-18n-#{key}"
|
64
62
|
end
|
65
|
-
|
66
|
-
|
67
|
-
|
63
|
+
|
64
|
+
brut_i18n_translation(**attributes)
|
65
|
+
|
66
|
+
end
|
68
67
|
end
|
69
68
|
end
|
@@ -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
|
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
|
8
|
-
|
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.
|
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[
|
21
|
-
default_html_attributes[
|
22
|
-
default_html_attributes[
|
23
|
-
default_html_attributes[
|
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[
|
28
|
+
default_html_attributes[:checked] = true
|
29
29
|
end
|
30
30
|
|
31
31
|
if !form.new? && !input.valid?
|