diff --git a/docs/guide/javascript_and_css.md b/docs/guide/javascript_and_css.md index 9ccb39a7d..876f5efcd 100644 --- a/docs/guide/javascript_and_css.md +++ b/docs/guide/javascript_and_css.md @@ -8,6 +8,202 @@ parent: How-to guide While ViewComponent doesn't provide any built-in tooling to do so, it’s possible to include JavaScript and CSS alongside components. + +## Propshaft / Stimulus + +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; + +### Upgrading a pre-Rails 8 app + +```ruby +# Gemfile (then run `bundle install`) +gem 'importmap-rails' # JavaScript version/digests without transpiling/bundling +gem 'propshaft' # Load static assets like JavaScript/CSS/images without transpilation/webpacker +gem 'stimulus-rails' # Hotwire JavaScript approach +``` + +```js +// app/javascript/controllers/application.js +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +application.debug = false +window.Stimulus = application + +export { application } +``` + +```js +// app/javascript/controllers/index.js +import { application } from "controllers/application" +``` + +```ruby +# config/importmap.rb +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "application", preload: true +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +``` + +### Approach 1 - Default _app/components_ ViewComponent directory using named Stimulus controllers, no autoloading + +Locate CSS and Stimulus js with a ViewComponent. This example demonstrates a _HelloWorldComponent_ in an _examples_ namespace with a sidecar file naming approach: + +``` +app/components +├── ... +├── examples +| ├── hello_world_component +| | ├── hello_world_component_controller.js +| | ├── hello_world_component.css +| | └── hello_world_component.html.erb +| └── hello_world_component.rb +├── ... +``` + +##### 1. Prepare _app/components_ as an asset path for css and ensure hot reloads of Stimulus JavaScript +```ruby +# config/application.rb +... +config.assets.paths << "app/components" +config.importmap.cache_sweepers << config.root.join("app/components") +... +``` + +##### 2. Pin ViewComponent Stimulus import map entries +```ruby +# config/importmap.rb +... +pin_all_from "app/components" +``` + +##### 3. Expose the Stimulus controller with a named key: + +```ruby +# app/javascript/controllers/index.js +... +import HelloWorldComponentController from "examples/hello_world_component/hello_world_component_controller"; +application.register("examples--hello-world-component", HelloWorldComponentController); +``` + +##### 4. Implement the ViewComponent with custom CSS and Stimulus behaviour: + +```ruby +# app/components/examples/hello_world_component.rb +class Examples::HelloWorldComponent < ViewComponent::Base + def initialize(title:) + @title = title + super + end +end +``` + +```erb + +<%= stylesheet_link_tag "examples/hello_world_component/hello_world_component" %> + +

<%= @title %>

+

<%= content %>

+ +
+

+ This div will be updated by the controller +

+ + +
+``` + +```css +/* app/components/examples/hello_world_component/hello_world_component.css */ +.hello-world { + color: blue; +} +``` + +```js +// app/components/examples/hello_world_component/hello_world_component_controller.js +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["output"] + + initialize() { + console.log("Component initialized!"); + } + + connect() { + console.log("Component connected!"); + + this.outputTarget.textContent = "This div has been initialised by stimulus and will be updated when you click the button" + } + + greet() { + const currentText = this.outputTarget.textContent; + this.outputTarget.textContent = currentText === "Hello from Stimulus!" + ? "Goodbye from Stimulus!" + : "Hello from Stimulus!"; + } +} +``` + +##### 5. Render the component in a rails view (or a [ViewComponent preview](previews.md)) to see the end result: + +```erb + + + ... + <%= render(Examples::HelloWorldComponent.new(title: "Hello World!")) { + "This will demonstrate the use of Stimulus and CSS in a ViewComponent".html_safe + } + %> + ... +``` + +### Approach 2 - Autoloaded ViewComponents in a sub-directory + +Stimulus controllers [will not currently autoload](https://github.com/ViewComponent/view_component/issues/1064#issuecomment-1163314487) if ViewComponents are located at: + +``` +app/components +``` + +a workaround is to put ViewComponents in a subdirectory: + +``` +app/frontend/components +``` + +and then autoload them in import map: + +```ruby +# config/importmap.rb +... +pin_all_from "app/frontend/components", under: "controllers", to: "components" +``` + +which also requires adjustment of the ViewComponent defaults to account for the sub-directory path: + +```ruby +# config/application.rb +config.autoload_paths << Rails.root.join("app/frontend/components") +config.importmap.cache_sweepers << Rails.root.join("app/frontend") +config.assets.paths << Rails.root.join("app/frontend") +config.view_component.view_component_path = "app/frontend/components" +``` + +allowing the autoloaded Stimulus controllers in views eg. +```erb + +... +
+... +``` + +## Webpacker + To use the Webpacker gem to compile assets located in `app/components`: 1. In `config/webpacker.yml`, add `"app/components"` to the `additional_paths` array (for example `additional_paths: ["app/components"]`). @@ -115,54 +311,3 @@ class Comment extends HTMLElement { } customElements.define('my-comment', Comment) ``` - -## Stimulus - -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`: - -```js -import { Application } from "stimulus" -import { definitionsFromContext } from "stimulus/webpack-helpers" - -const application = Application.start() -const context = require.context("controllers", true, /\.js$/) -const contextComponents = require.context("../../components", true, /_controller\.js$/) -application.load( - definitionsFromContext(context).concat( - definitionsFromContext(contextComponents) - ) -) -``` - -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. - -After configuring Webpack to load Stimulus controller files from the `components` directory, add the path to `additional_paths` in `config/webpacker.yml`: - -```yml - additional_paths: ["app/components"] -``` - -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]( -https://stimulusjs.org/handbook/installing#controller-filenames-map-to-identifiers): - -```console -app/components -├── ... -├── example -| ├── component.rb -| ├── component.css -| ├── component.html.erb -| └── component_controller.js -├── ... -``` - -`component_controller.js`'s Stimulus identifier becomes: `example--component`: - -```erb -
- - -
-``` - -See [Generators Options](generators.html#generate-a-stimulus-controller) to generate a Stimulus controller alongside the component using the generator.