Skip to content

Commit f2adf98

Browse files
committed
Document a propshaft approach to JS/CSS
Resolves #1064 Given Rails 8 has been released without transpilation or bundled JavaScript by default, this documentation update demonstrates how to use Stimulus and CSS in a ViewComponent without webpacker. A more terse demonstration would be possible without the view component arguments and/or less complicated Stimulus behaviour however that might be less instructive. One of the trickier aspects of binding Stimulus controllers is often determining the correct data-controller key to use, especially when namespaces and/or multi-word component names are involved which this example makes explicit. Approach 2 demonstrates the Stimulus behaviour but not the css, that would likely involve dartsass-rails
1 parent 50bd2a8 commit f2adf98

File tree

2 files changed

+197
-51
lines changed

2 files changed

+197
-51
lines changed

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ nav_order: 5
1010

1111
## main
1212

13+
* Add documentation for JavaScript and CSS without webpacker
14+
15+
*Jason Kotchoff*
16+
1317
* Ensure HTML output safety wrapper is used for all inline templates.
1418

1519
*Joel Hawksley*

docs/guide/javascript_and_css.md

Lines changed: 193 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,199 @@ parent: How-to guide
88

99
While ViewComponent doesn't provide any built-in tooling to do so, it’s possible to include JavaScript and CSS alongside components.
1010

11+
## Propshaft / Stimulus
12+
13+
To use a [transpiler-less and bundler-less approach to JavaScript](https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755) (the default for Rails 8), Stimulus and CSS can be used inside ViewComponents one of two ways:
14+
15+
### Upgrading a pre-Rails 8 app
16+
17+
```ruby
18+
# Gemfile (then run `bundle install`)
19+
gem 'importmap-rails' # JavaScript version/digests without transpiling/bundling
20+
gem 'propshaft' # Load static assets like JavaScript/CSS/images without transpilation/webpacker
21+
gem 'stimulus-rails' # Hotwire JavaScript approach
22+
```
23+
24+
```js
25+
// app/javascript/controllers/application.js
26+
import { Application } from "@hotwired/stimulus"
27+
28+
const application = Application.start()
29+
30+
application.debug = false
31+
window.Stimulus = application
32+
33+
export { application }
34+
```
35+
36+
```js
37+
// app/javascript/controllers/index.js
38+
import { application } from "controllers/application"
39+
```
40+
41+
```ruby
42+
# config/importmap.rb
43+
pin "@hotwired/turbo-rails", to: "turbo.min.js"
44+
pin "application", preload: true
45+
pin "@hotwired/stimulus", to: "stimulus.min.js"
46+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
47+
```
48+
49+
### Approach 1 - Default _app/components_ ViewComponent directory using named Stimulus controllers, no autoloading
50+
51+
Locate CSS and Stimulus js with a ViewComponent. This example demonstrates a _HelloWorldComponent_ in an _examples_ namespace with a sidecar file naming approach:
52+
53+
```console
54+
app/components
55+
├── ...
56+
├── examples
57+
| ├── hello_world_component
58+
| | ├── hello_world_component_controller.js
59+
| | ├── hello_world_component.css
60+
| | └── hello_world_component.html.erb
61+
| └── hello_world_component.rb
62+
├── ...
63+
```
64+
65+
#### 1. Prepare _app/components_ as an asset path for css and ensure hot reloads of Stimulus JavaScript
66+
67+
```ruby
68+
# config/application.rb
69+
config.assets.paths << "app/components"
70+
config.importmap.cache_sweepers << config.root.join("app/components")
71+
```
72+
73+
#### 2. Pin ViewComponent Stimulus import map entries
74+
75+
```ruby
76+
# config/importmap.rb
77+
pin_all_from "app/components"
78+
```
79+
80+
#### 3. Expose the Stimulus controller with a named key:
81+
82+
```ruby
83+
# app/javascript/controllers/index.js
84+
import HelloWorldComponentController from "examples/hello_world_component/hello_world_component_controller";
85+
application.register("examples--hello-world-component", HelloWorldComponentController);
86+
```
87+
88+
#### 4. Implement the ViewComponent with custom CSS and Stimulus behaviour:
89+
90+
```ruby
91+
# app/components/examples/hello_world_component.rb
92+
class Examples::HelloWorldComponent < ViewComponent::Base
93+
def initialize(title:)
94+
@title = title
95+
super
96+
end
97+
end
98+
```
99+
100+
```erb
101+
<!-- app/components/examples/hello_world_component/hello_world_component.html.erb -->
102+
<%= stylesheet_link_tag "examples/hello_world_component/hello_world_component" %>
103+
104+
<h1><%= @title %></h1>
105+
<p><%= content %></p>
106+
107+
<div data-controller="examples--hello-world-component">
108+
<p class="hello-world" id="<%= @id %>" data-examples--hello-world-component-target="output">
109+
This div will be updated by the controller
110+
</p>
111+
112+
<button data-action="click->examples--hello-world-component#greet">Toggle Greeting</button>
113+
</div>
114+
```
115+
116+
```css
117+
/* app/components/examples/hello_world_component/hello_world_component.css */
118+
.hello-world {
119+
color: blue;
120+
}
121+
```
122+
123+
```js
124+
// app/components/examples/hello_world_component/hello_world_component_controller.js
125+
import { Controller } from "@hotwired/stimulus";
126+
127+
export default class extends Controller {
128+
static targets = ["output"]
129+
130+
initialize() {
131+
console.log("Component initialized!");
132+
}
133+
134+
connect() {
135+
console.log("Component connected!");
136+
137+
this.outputTarget.textContent = "This div has been initialised by stimulus and will be updated when you click the button"
138+
}
139+
140+
greet() {
141+
const currentText = this.outputTarget.textContent;
142+
this.outputTarget.textContent = currentText === "Hello from Stimulus!"
143+
? "Goodbye from Stimulus!"
144+
: "Hello from Stimulus!";
145+
}
146+
}
147+
```
148+
149+
#### 5. Render the component in a rails view (or a [ViewComponent preview](previews.md)) to see the end result:
150+
151+
```erb
152+
<!-- app/views/layouts/application.html.erb -->
153+
<body>
154+
...
155+
<%= render(Examples::HelloWorldComponent.new(title: "Hello World!")) {
156+
"<em>This</em> will demonstrate the use of <b>Stimulus</b> and <b>CSS</b> in a ViewComponent".html_safe
157+
}
158+
%>
159+
...
160+
```
161+
162+
### Approach 2 - Autoloaded ViewComponents in a sub-directory
163+
164+
Stimulus controllers [won't currently autoload](https://github.com/ViewComponent/view_component/issues/1064#issuecomment-1163314487) if ViewComponents are located at:
165+
166+
```console
167+
app/components
168+
```
169+
170+
a workaround is to put ViewComponents in a subdirectory:
171+
172+
```console
173+
app/frontend/components
174+
```
175+
176+
and then autoload them in import map:
177+
178+
```ruby
179+
# config/importmap.rb
180+
pin_all_from "app/frontend/components", under: "controllers", to: "components"
181+
```
182+
183+
which also requires adjustment of the ViewComponent defaults to account for the sub-directory path:
184+
185+
```ruby
186+
# config/application.rb
187+
config.autoload_paths << Rails.root.join("app/frontend/components")
188+
config.importmap.cache_sweepers << Rails.root.join("app/frontend")
189+
config.assets.paths << Rails.root.join("app/frontend")
190+
config.view_component.view_component_path = "app/frontend/components"
191+
```
192+
193+
allowing the autoloaded Stimulus controllers in views eg.
194+
195+
```erb
196+
<!-- app/components/examples/hello_world_component/hello_world_component.html.erb -->
197+
...
198+
<div data-controller="examples--hello-world-component--hello-world-component">
199+
...
200+
```
201+
202+
## Webpacker
203+
11204
To use the Webpacker gem to compile assets located in `app/components`:
12205

13206
1. In `config/webpacker.yml`, add `"app/components"` to the `additional_paths` array (for example `additional_paths: ["app/components"]`).
@@ -115,54 +308,3 @@ class Comment extends HTMLElement {
115308
}
116309
customElements.define('my-comment', Comment)
117310
```
118-
119-
## Stimulus
120-
121-
In Stimulus, create a 1:1 mapping between a Stimulus controller and a component. To load in Stimulus controllers from the `app/components` tree, amend the Stimulus boot code in `app/javascript/controllers/index.js`:
122-
123-
```js
124-
import { Application } from "stimulus"
125-
import { definitionsFromContext } from "stimulus/webpack-helpers"
126-
127-
const application = Application.start()
128-
const context = require.context("controllers", true, /\.js$/)
129-
const contextComponents = require.context("../../components", true, /_controller\.js$/)
130-
application.load(
131-
definitionsFromContext(context).concat(
132-
definitionsFromContext(contextComponents)
133-
)
134-
)
135-
```
136-
137-
This enables the creation of files such as `app/components/widget_controller.js`, where the controller identifier matches the `data-controller` attribute in the component's HTML template.
138-
139-
After configuring Webpack to load Stimulus controller files from the `components` directory, add the path to `additional_paths` in `config/webpacker.yml`:
140-
141-
```yml
142-
additional_paths: ["app/components"]
143-
```
144-
145-
When placing a Stimulus controller inside a sidecar directory, be aware that when referencing the controller [each forward slash in a namespaced controller file’s path becomes two dashes in its identifier](
146-
https://stimulusjs.org/handbook/installing#controller-filenames-map-to-identifiers):
147-
148-
```console
149-
app/components
150-
├── ...
151-
├── example
152-
| ├── component.rb
153-
| ├── component.css
154-
| ├── component.html.erb
155-
| └── component_controller.js
156-
├── ...
157-
```
158-
159-
`component_controller.js`'s Stimulus identifier becomes: `example--component`:
160-
161-
```erb
162-
<div data-controller="example--component">
163-
<input type="text">
164-
<button data-action="click->example--component#greet">Greet</button>
165-
</div>
166-
```
167-
168-
See [Generators Options](generators.html#generate-a-stimulus-controller) to generate a Stimulus controller alongside the component using the generator.

0 commit comments

Comments
 (0)