Implement BaseComponent for per-component script packs#5475
Conversation
**Why**: To give developers a simple way to write accompanying JavaScript for a ViewComponent, in a way which limits boilerplate and supports potential reuse for view components outside Rails. Also further reinforces the idea of components as self-contained units, limiting the amount of JavaScript rendered to a page to only that which is used in the page.
app/components/base_component.rb
Outdated
| define_method 'render_in' do |view_context, &block| | ||
| BaseComponent.rendered_scripts |= [script] | ||
| super(view_context, &block) | ||
| end |
There was a problem hiding this comment.
Room for micro-optimization here: We only need to append the script once for the first call to render_in. It'd be nice if it could be possible to unwrap the method or at least short-circuit as quickly / efficiently as we can here after the first call.
app/components/base_component.rb
Outdated
| @@ -0,0 +1,14 @@ | |||
| class BaseComponent < ViewComponent::Base | |||
| @rendered_scripts = [] | |||
There was a problem hiding this comment.
Class variable is a bad idea, since classes won't be reloaded on each request in production (config.cache_classes), so rendered scripts would be persist between requests. Better idea may be to call a method / variable on view_context within the wrapped render_in.
|
Hm, unfortunately, it looks like USWDS components which are "accordion-like" (e.g. the "Official USA website" banner) assume that the accordion scripts have been loaded and initialized, so loading them only when an accordion component is present causes some unexpected breakage. Ideally the banner would be self-contained, so that running In the meantime, perhaps...
|
Required for Babel to be used to transform source https://github.com/rails/webpacker/blob/e0c998e2aa7f096709eaa2a7f2d7e29d413abed3/package/rules/babel.js#L10
app/components/base_component.rb
Outdated
| @@ -0,0 +1,10 @@ | |||
| class BaseComponent < ViewComponent::Base | |||
| class << self | |||
| def renders_script(script = self.name.underscore) | |||
There was a problem hiding this comment.
WDYT about removing the default script name? I think it would be kinder to our future selves to trace if we saw app/components/accordion_component.js in component (or even just accordion_component.js)
There was a problem hiding this comment.
WDYT about removing the default script name? I think it would be kinder to our future selves to trace if we saw
app/components/accordion_component.jsin component (or even justaccordion_component.js)
By this, do you mean having the component specify the name of the script(s) it renders?
e.g. instead of render_script, something like renders_script 'accordion_component' ?
There was a problem hiding this comment.
yup! I was thinking super extra explicit, like this even
renders_script 'app/components/accordion_component.js'
There was a problem hiding this comment.
or maybe renders_script './accordion_component.js'
There was a problem hiding this comment.
We could make that work, but I expect it'd require some faking, since the scripts are meant to be the names of the packs generated by Webpack, which are just the base name of the script itself (source).
That being said:
- Since we have to brute-force recreate this behavior in
config/webpack/environment.jsto add our additional component scripts, we could customize this as we want. - Thinking in terms of how we might someday reuse these components outside Rails, it'd probably be best to avoid assumptions about Webpacker.
To this latter point, this is also part of the reason I'd tried to use the class variable and avoid view_context, to avoid assuming some helper javascript_packs_tag_once exists. With what you mention here, what I'm thinking is maybe we check for an abstract method in the view context which is assumed to be implemented in a consuming project. In Rails, we'd implement it by taking a path name and mapping it to the Webpacker pack.
define_method 'render_in' do |view_context, &block|
view_context.render_component_script(script) if view_context.respond_to?(:render_component_script)
super(view_context, &block)
endmodule ScriptHelper
def render_component_script(path)
javascript_packs_tag_once(File.basename(path, ".*"))
end
endThere was a problem hiding this comment.
Didn't mean to focus on the extension or path, I just mean having a filename to try to trace, I think even having renders_script 'accordion_component' would be clearer than renders_script
There was a problem hiding this comment.
Hm, on the other hand, the extra-explicitness feels rather at odds with how ViewComponent already works with assuming a template file .html.erb of the same base name as the class implementation (source) 🤔
This implementation was meant to build off that:
Before:
accordion_component.rb
accordion_component.html.erb
After:
accordion_component.rb
accordion_component.html.erb
accordion_component.js
There was a problem hiding this comment.
This is not the hill I die on, I think the rails magic pattern of "just know what the filename is supposed to be" is convenient but not something I'd extend. It's common enough that it's not worth fighting against
There was a problem hiding this comment.
Hm, in that case, for now I think I lie more on the side of keeping it implicit, at least for consistency with ViewComponent and what it refers to as "sidecar files".
There was a problem hiding this comment.
Regardless, I did favor the idea of an abstract render_component_script method, so I implemented some simple aliasing in 138f944.
| @@ -0,0 +1,43 @@ | |||
| require 'rails_helper' | |||
|
|
|||
| RSpec.describe BaseComponent, type: :component do | |||
There was a problem hiding this comment.
do we do anything for type: :component tests?
There was a problem hiding this comment.
This pulls in ViewComponent and Capybara test helpers.
identity-idp/spec/rails_helper.rb
Lines 43 to 44 in 54fc578
Reduce tight coupling to Rails/Webpacker
Until we can find a way to incorporate implicit dependency from accordion-like UI such as banner
For now, I reverted the accordion script in eb2d5c5. Which means this pull request doesn't yet do anything functional, but as mentioned previously, I'd already planned to (and have since started to) build on this work for LG-5192, so it'd still prove valuable as a basis. |
- No separate call to render_script required - Builds on existing ViewComponent behavior - Computed once as class variable - Memoize enqueue
Why: To give developers a simple way to write accompanying JavaScript for a ViewComponent, in a way which limits boilerplate and supports potential reuse for view components outside Rails. Also further reinforces the idea of components as self-contained units, limiting the amount of JavaScript rendered to a page to only that which is used in the page.
Related resources: