Skip to content

Implement BaseComponent for per-component script packs#5475

Merged
aduth merged 10 commits intomainfrom
aduth-component-scripts
Oct 14, 2021
Merged

Implement BaseComponent for per-component script packs#5475
aduth merged 10 commits intomainfrom
aduth-component-scripts

Conversation

@aduth
Copy link
Contributor

@aduth aduth commented Oct 7, 2021

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:

**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.
Comment on lines +8 to +11
define_method 'render_in' do |view_context, &block|
BaseComponent.rendered_scripts |= [script]
super(view_context, &block)
end
Copy link
Contributor Author

@aduth aduth Oct 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -0,0 +1,14 @@
class BaseComponent < ViewComponent::Base
@rendered_scripts = []
Copy link
Contributor Author

@aduth aduth Oct 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use view_context in f1c022d.

@aduth
Copy link
Contributor Author

aduth commented Oct 7, 2021

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 banner.on() would include whatever accordion behaviors it's expecting.

In the meantime, perhaps...

  • I leave in the scripts support, but back out the specific accordion_component.js implementation for now
    • This pull request is partially intended in support of LG-5193, where I'd hoped to use the approach for componentization of the international phone field.
  • Or: Refactor banner (and other accordion-likes) as a component, and make sure that their scripts include the prerequisite accordion initialization.

Copy link
Contributor

@zachmargolis zachmargolis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -0,0 +1,10 @@
class BaseComponent < ViewComponent::Base
class << self
def renders_script(script = self.name.underscore)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

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' ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup! I was thinking super extra explicit, like this even

renders_script 'app/components/accordion_component.js'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe renders_script './accordion_component.js'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.js to 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)
end
module ScriptHelper
  def render_component_script(path)
    javascript_packs_tag_once(File.basename(path, ".*"))
  end
end

Copy link
Contributor

@zachmargolis zachmargolis Oct 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we do anything for type: :component tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pulls in ViewComponent and Capybara test helpers.

config.include ViewComponent::TestHelpers, type: :component
config.include Capybara::RSpecMatchers, type: :component

aduth added 3 commits October 7, 2021 12:30
Reduce tight coupling to Rails/Webpacker
Until we can find a way to incorporate implicit dependency from accordion-like UI such as banner
@aduth
Copy link
Contributor Author

aduth commented Oct 7, 2021

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.

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
@aduth aduth merged commit fe128d2 into main Oct 14, 2021
@aduth aduth deleted the aduth-component-scripts branch October 14, 2021 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants