Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to configure stimulus using the new rails 7 asset pipeline #1064

Open
brunoprietog opened this issue Sep 9, 2021 · 33 comments
Open

How to configure stimulus using the new rails 7 asset pipeline #1064

brunoprietog opened this issue Sep 9, 2021 · 33 comments

Comments

@brunoprietog
Copy link

Feature request

Hello, how could this be done? In the javascript documentation the configuration needed to use webpack is indicated, but I am interested in using stimulus using importmap and sprockets, as it will be in rails 7 by default, using the asset pipeline.

Could this be added to the documentation?

Thanks!

Motivation

Motivated by those post by DHH:

@joelhawksley
Copy link
Member

@brunoprietog I'd be happy to add it to the documentation. Are you able to look into this?

@liaden
Copy link

liaden commented Sep 13, 2021

I got this hooked up today. To do this you should:

  1. Make sure you are using this change which is in version 0.5.2.
  2. Add pin_all_from "app/components" to config/importmaps.rb
  3. Add Rails.application.config.assets.paths << "app/components" to config/initializers/assets.rb
  4. Add Rails.application.config.assets.debug = true to config/initializers/assets.rb. I double checked this one by removing it, and sadly it is needed. Maybe there is a better configuration that would alleviate this?
  5. Add config.assets.check_precompiled_asset = false to config/environments/development.rb. Same as item 4, but without this we get an error page.

I was initializing my component with be rails g component hello_world --sidecar --stimulus for what it is worth.

@seanharmer
Copy link

I'm struggling to get this working with Rails 7 using the esbuild approach too. So far I've had to put the view_component stimulus controllers within the app/javascripts hierarchy. esbuild complains if I add import "../components" to the application.js file.

@roelandmoors
Copy link

I got this hooked up today. To do this you should:

  1. Make sure you are using this change which is in version 0.5.2.
  2. Add pin_all_from "app/components" to config/importmaps.rb
  3. Add Rails.application.config.assets.paths << "app/components" to config/initializers/assets.rb
  4. Add Rails.application.config.assets.debug = true to config/initializers/assets.rb. I double checked this one by removing it, and sadly it is needed. Maybe there is a better configuration that would alleviate this?
  5. Add config.assets.check_precompiled_asset = false to config/environments/development.rb. Same as item 4, but without this we get an error page.

I was initializing my component with be rails g component hello_world --sidecar --stimulus for what it is worth.

Maybe this needs to be added in app/assets/config/manifest.js?
//= link_tree ../../components .js

@roelandmoors
Copy link

I think you also need to set the cache_sweeper to refresh the importmap: https://github.com/rails/importmap-rails#sweeping-the-cache-in-development-and-test

@roelandmoors
Copy link

roelandmoors commented Oct 16, 2021

I'm struggling to get this working with Rails 7 using the esbuild approach too. So far I've had to put the view_component stimulus controllers within the app/javascripts hierarchy. esbuild complains if I add import "../components" to the application.js file.

I don't have a problem when using esbuild.

In app/javascript/controller/index.js I add an import like this:
import AlertController from "../../components/alert_controller";
and then I can register it:
application.register("alert", AlertController);

@chunlea
Copy link

chunlea commented Nov 21, 2021

I come up with a solution based on https://github.com/hotwired/stimulus-rails/blob/main/lib/tasks/stimulus_tasks.rake#L29-L46. I use esbuild with jsbundling-rails.

So I added a view_component.rake to my project, with those content:

# frozen_string_literal: true

namespace :view_component do
  namespace :stimulus_manifest do
    task display: :environment do
      puts Stimulus::Manifest.generate_from(Rails.root.join('app/components'))
    end

    task update: :environment do
      manifest =
        Stimulus::Manifest.generate_from(Rails.root.join('app/components'))

      File.open(Rails.root.join('app/components/index.js'), 'w+') do |index|
        index.puts '// This file is auto-generated by ./bin/rails view_component:stimulus_manifest:update'
        index.puts '// Run that command whenever you add a new controller in ViewComponent'
        index.puts
        index.puts %(import { application } from "../javascript/controllers/application")
        index.puts manifest
      end
    end
  end
end

if Rake::Task.task_defined?('stimulus:manifest:update')
  Rake::Task['stimulus:manifest:update'].enhance do
    Rake::Task['view_component:stimulus_manifest:update'].invoke
  end
end

And it will generate file app/components/index.js everytime you run the stimulus:manifest:update. Why I put this file inside app/components/ is because stimulus-rails only generate the manifest content based on relative of /app/javascript/controllers/.

And then we can just simple use import '../components'; in app/javascript/application.js.

And it works well for me.

@ViewComponent ViewComponent deleted a comment Feb 9, 2022
@wdiechmann
Copy link

@chunlea thank you so much for sharing! I've been banging my head on this too many hours!

🥳

@seanbjornsson
Copy link

I have just started a new Rails 7 project, with View Component and Stimulus. I am working with just vanilla Importmap Rails, no jsbundling-rails or esbuild. My file structure is as created by the component generator with the sidecar and stimulus flags as @liaden was using.

Has anyone found a solution that automatically loads sidecar stimulus controllers as they are added without having to register them manually, using only importmap-rails or the vanilla asset pipeline/sprockets?

I have a feeling this sort of abuses what importmap was built to do, so I wouldn't be surprised if the answer was no. But I also feel like there must be a way to accomplish this. Honestly, this could also be more of a question for the Importmap team, but it's all based on trying to sidecar stimulus controllers with ViewComponents, so here I am...
I was hoping this would be the case (automatic registration) with the suggestions above, but that is not the case (for me at least)

I've tried:

  • Turning app/javascript/controllers/index.js into an erb file and doing a little metaprogramming to cycle through all the controllers in app/components/**/*_controller.js and using the manual registration technique that is already working.
    • no dice. I think it's cause it doesn't get processed into a js file before importmap looks to pin it. Honestly not sure at this point. This approach might work eventually, but it feels very hacky.
  • Dynamically (iterate over files using ruby) calling pin_all_for in importmap.rb on each component dir. I was hoping I could do this using the under: 'components' flag to add these controllers to the "controllers/" prefix in the import map.
    • However, this doesn't work because importmap really just seems to take the prefix off your file path. Ex stimulus install adds pin_all_from "app/javascript/controllers", under: "controllers" but all it really does is remove app/javascript from the path when requiring it. (I honestly don't think that's the whole story, but I'm still slowly working through the importmap code).
    • Therefore you get this error (cause you're basically trying to change the "directory" that the asset lives in
    The asset "controllers/hello_component/hello_component_controller.js" is not present in the asset pipeline.
    Importmap skipped missing path: controllers/hello_component/hello_component_controller.js
    
  • Adding //=link_tree ../../components .js to app/assets/config/manifest.js. I honestly thought this would work but it doesn't. I'm not familiar enough yet with debugging the asset pipeline to troubleshoot this approach.

Following @liaden 's suggestions, I can get a stimulus controller working in a sidecar folder only by also using @roelandmoors 's suggestion of manually loading and registering the controller. see below for what I'm starting with.

File structure

├── hello_component
│   ├── hello_component_controller.js
│   └── hello_component.html.erb
└── hello_component.rb

config/initializers/assets.rb

Rails.application.config.assets.paths << "app/components"
Rails.application.config.assets.debug = true

config/importmap.rb

pin "application", preload: true
# ... Hotwire stuff ...
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/components" <- new line

app/javascript/controllers/index.js

// Import and register all your controllers from the importmap under controllers/*

import { application } from "controllers/application"

// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

import HelloComponentController from "hello_component/hello_component_controller";
application.register("hello-component", HelloComponentController);

and I actually didn't need step 5 (updating development.rb).

Anyway, I'm excited about ViewComponent, and will use it if I can't get this automatically loading sidecar controllers thing to work. But it would be pretty nice! I'll post back if I figure it out....

@imustafin
Copy link

I've came up with a very dirty (IMHO) solution a while ago, but waited for someone to suggest a cleaner one 😅

My solution doesn't require manual controller registration.

The main idea is to add app to asset paths so that we have our controllers by the components/controller.js path in the asset pipeline. To make it a bit cleaner, we add it as the last path:

# config/application.rb

initializer "app_assets", after: "importmap.assets" do
  Rails.application.config.assets.paths << Rails.root.join('app') # for component sidecar js
end

# Sweep importmap cache for components
config.importmap.cache_sweepers << Rails.root.join('app/components')

Then we can add component JS files to the asset pipeline:

// app/assets/config/manifest.js

//= link_tree ../../components .js

Now importmap can pick them up:

# config/importmap.rb

pin_all_from "app/components", under: "components"

Optionally, lazy load controllers:

// app/javascript/controllers/index.js

import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
lazyLoadControllersFrom("components", application)

That's it.

My hobby app has all of this in one commit together with a sample component imustafin/sibrowser@12fb4ae.

Running this setup for a month, faced no problems yet.

Possible drawback
Unwanted application code from the app dir can get included into the asset pipeline, if you are not careful enough in the manifest

I'm not sure if this can be "the officially recommended way", but if so, I could send a PR with the documentation.

@seanbjornsson
Copy link


Wow, thank you so much @imustafin !! I was hoping hoping that someone out there had figured this out.
From the perspective of someone just finding ViewComponents, your post is exactly the documentation I was looking for but couldn't find. So I support some documentation!

I'm also curious what people think about an install task something like rails viewcomponent:install --sidecar --importmap (sorta clunky, but you get the idea).
Although I need to read some more code, I'm unfamiliar if any of this exists already. And/or if there's a neat way to wrap this all into the gem or a single file/config.

@spnv
Copy link

spnv commented Mar 20, 2022

@imustafin For me this one is not optional to make it works.

// app/javascript/controllers/index.js

import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
lazyLoadControllersFrom("components", application)

@imustafin
Copy link

@spnv yes, you need to load them somehow. You can do lazyLoadControllersFrom or eagerLoadControllersFrom in app/javascript/controllers/index.js, or you can load them some other way.

Maybe the word "optional" was wrong there. It should have been "for example" maybe.

@tripptuttle
Copy link

So, @imustafin 's solution is working great, if my controller.js files are directly under the top /components folder. Besides doing a whole lot of looping in importmap.rb, anyone have any ideas for getting it to work with namespaced folders and sidecars? @seanbjornsson , you mentioned you were using sidecar dirs, and it's working for you auto-loading the controller for a view component without adding anything to the .erb for a component?

@imustafin
Copy link

imustafin commented Jun 7, 2022

@tripptuttle if I understood you correctly, my solution should work. In the mentioned commit imustafin/sibrowser@12fb4ae we have app/components/packages_paginated/component_controller.js which is not directly in /components but in a namespace/sidecar for the packages_paginated component. Or does namespace/sidecar mean something different? Maybe you would need to fiddle a bit with the name in data-action="" to add the folder name using -- (https://viewcomponent.org/guide/javascript_and_css.html#stimulus)

@tripptuttle
Copy link

Thanks for getting back so quick! Yes, you are understanding correctly, and you are right! I forgot to update data-controller, I had only updated data-action with the namespace! :-) Thank you!

Everything is working, but do you get the following console errors from stimulus? It seems it's looking at the top level as well for the controller.

2stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:72 Failed to autoload controller: tabs-component TypeError: Failed to resolve module specifier 'controllers/tabs_component_controller'
    at loadController (stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:70:12)
    at stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:39:65
    at Array.forEach (<anonymous>)
    at lazyLoadExistingControllers (stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:39:39)
    at MutationObserver.observe.attributeFilter (stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:53:11)
(anonymous) @ stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:72
Promise.catch (async)
loadController @ stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:72
(anonymous) @ stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:39
lazyLoadExistingControllers @ stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:39
MutationObserver.observe.attributeFilter @ stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:53
childList (async)
(anonymous) @ tabs_component_controller-dea3ce95bd03edc11d91728aaa583981a52ab5e007156b5059827962a5303d98.js:28
Promise.then (async)
(anonymous) @ tabs_component_controller-dea3ce95bd03edc11d91728aaa583981a52ab5e007156b5059827962a5303d98.js:23
Promise.then (async)
switchTab @ tabs_component_controller-dea3ce95bd03edc11d91728aaa583981a52ab5e007156b5059827962a5303d98.js:20
invokeWithEvent @ controller.ts:12
handleEvent @ controller.ts:12
handleEvent @ controller.ts:12
2stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:72 Failed to autoload controller: tabs-component TypeError: Failed to resolve module specifier 'components/tabs_component_controller'
    at loadController (stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:70:12)
    at stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:39:65
    at Array.forEach (<anonymous>)
    at lazyLoadExistingControllers (stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:39:39)
    at MutationObserver.observe.attributeFilter (stimulus-loading-e113e6e671b6ac98f288e73025b7d6178cc4efba3a96bcbb838d5e125537622f.js:53:11)

@imustafin
Copy link

To be honest, it has been a long time since I've configured all of this :)

I don't remember if I had such errors. I believe I had only the the expected shim error on Firefox. I don't remember if I even checked the console in other browsers. It just worked.

@tripptuttle
Copy link

Yeah, it all works, but it just throws an error I think while resolving the request for the controller out of the import map.

@PedroAugustoRamalhoDuarte

I come up with a solution based on https://github.com/hotwired/stimulus-rails/blob/main/lib/tasks/stimulus_tasks.rake#L29-L46. I use esbuild with jsbundling-rails.

So I added a view_component.rake to my project, with those content:

# frozen_string_literal: true

namespace :view_component do
  namespace :stimulus_manifest do
    task display: :environment do
      puts Stimulus::Manifest.generate_from(Rails.root.join('app/components'))
    end

    task update: :environment do
      manifest =
        Stimulus::Manifest.generate_from(Rails.root.join('app/components'))

      File.open(Rails.root.join('app/components/index.js'), 'w+') do |index|
        index.puts '// This file is auto-generated by ./bin/rails view_component:stimulus_manifest:update'
        index.puts '// Run that command whenever you add a new controller in ViewComponent'
        index.puts
        index.puts %(import { application } from "../javascript/controllers/application")
        index.puts manifest
      end
    end
  end
end

if Rake::Task.task_defined?('stimulus:manifest:update')
  Rake::Task['stimulus:manifest:update'].enhance do
    Rake::Task['view_component:stimulus_manifest:update'].invoke
  end
end

And it will generate file app/components/index.js everytime you run the stimulus:manifest:update. Why I put this file inside app/components/ is because stimulus-rails only generate the manifest content based on relative of /app/javascript/controllers/.

And then we can just simple use import '../components'; in app/javascript/application.js.

And it works well for me.

Thanks for this rake, it works very well with esbuild, i have add some lines to automatic import the view component stimulus manifest inside normal stimulus controller manifest

Add this lines at the end of update task:

File.open(Rails.root.join('app/javascript/controllers/index.js'), 'a') do |index|
  index.puts ''
  index.puts '// Import view component stimulus controllers'
  index.puts %(import "../../components")
end

@progapandist
Copy link
Contributor

I wonder if there is any change we can upstream on Stimulus side to make registering non-standard locations for controllers more straightforward. Otherwise, it just seems hacky for now :(

Any ideas? I'd be happy to try to push this forward.

@Spone
Copy link
Collaborator

Spone commented Jun 14, 2022

👋 @progapandist nice to see you here!

I'd be happy to spend some time to pair on this and try to figure out the best way forward. Let me know!

@progapandist
Copy link
Contributor

@Spone — small world! 😆

For now we did an ugly workaround with adding app/frontend/components (has to be a nested folder to avoid preloading whole app) to eager_load_paths in application.rb. We are not satisfied with this long-term though. Let's find time to pair next week. I think you still have my number :) If not — contact me at andrey@hey.com

@progapandist
Copy link
Contributor

Hey @Spone, I have created a repo with a test application that demonstrates both the "nested folder" workaround, and the failing attempt to register non-standardly-located Stimulus controllers. I have took time to describe everything in the README.

https://github.com/progapandist/importmap-view-component-stimulus/

So far I cannot pinpoint which library is to blame for the incompatibility, but I suspect sprockets-rails or sprockets. Seems like importmap-rails does its job to pin the app/components, but the Asset Pipeline does not generate digest for controller JS files and does not put them into /public.

I would appreciate any further guidance!

@vsppedro
Copy link
Contributor

vsppedro commented Aug 3, 2022

I was trying to achieve this and asked for help on StackOverflow: https://stackoverflow.com/questions/73223634/how-to-make-viewcomponent-works-with-importmap-rails/73228193#73228193

I'm using the first option and it's working like a charm.

How do you feel about adding the first option to the documentation?

@unikitty37
Copy link

It gets a bit messy when using namespaced components and sidecar folders. If I use rails g Page::DarkModeSelector to generate Page::DarkModeSelectorComponent I have to use data-controller="page--dark-mode-selector-component--dark-mode-selector-component".

Granted, that's a fairly long name to start with, but is there a way to make these a little (well, a lot TBH :) less verbose? Having the controller name just be page--dark-mode-selector-component would be ideal.

@PedroAugustoRamalhoDuarte

It gets a bit messy when using namespaced components and sidecar folders. If I use rails g Page::DarkModeSelector to generate Page::DarkModeSelectorComponent I have to use data-controller="page--dark-mode-selector-component--dark-mode-selector-component".

Granted, that's a fairly long name to start with, but is there a way to make these a little (well, a lot TBH :) less verbose? Having the controller name just be page--dark-mode-selector-component would be ideal.

@unikitty37 we talk about that in this issue: #1393

@khash
Copy link

khash commented Jan 26, 2023

Building on top of @chunlea 's work, I added a section to the rake task to include any CSS in components folder as well:

# frozen_string_literal: true

namespace :view_component do
  namespace :stimulus_manifest do
    task display: :environment do
      puts Stimulus::Manifest.generate_from(Rails.root.join("app/components"))
    end

    task update: :environment do
      manifest =
        Stimulus::Manifest.generate_from(Rails.root.join("app/components"))

      File.open(Rails.root.join("app/components/index.js"), "w+") do |index|
        index.puts "// This file is auto-generated by ./bin/rails view_component:stimulus_manifest:update"
        index.puts "// Run that command whenever you add a new controller in ViewComponent"
        index.puts
        index.puts %(import { application } from "../javascript/controllers/application")
        index.puts manifest
      end

      # get all css files under app/components
      css_files = Dir.glob(Rails.root.join("app/components/**/*.css"))
      # remove the path 
      css_files = css_files.map { |file| file.gsub(Rails.root.join("app/components/").to_s, "./") }
      # remove self reference
      css_files = css_files.reject { |file| file == "./components.css" }
      # wrap each item in a css import
      css_files = css_files.map { |file| "@import '#{file}';" }

      File.open(Rails.root.join("app/components/components.css"), "w+") do |index|
        index.puts "/* This file is auto-generated by ./bin/rails view_component:stimulus_manifest:update */"
        index.puts "/* Run that command whenever you add a new controller in ViewComponent */"
        index.puts
        index.puts css_files
      end
    end
  end
end

if Rake::Task.task_defined?("stimulus:manifest:update")
  Rake::Task["stimulus:manifest:update"].enhance do
    Rake::Task["view_component:stimulus_manifest:update"].invoke
  end
end

@ksouthworth
Copy link

@imustafin thank you! I was banging my head against the wall with this, but you're configuration is working for me.

I had something very close to that but it was only working in my local Rails development environment and was breaking in production until I adopted your config.

@maks112v
Copy link

maks112v commented May 26, 2023

tldr: RailsByte for easy setup

I have found myself returning to this thread several times, so I have created a RailsByte that includes the code mentioned earlier and adds an import example. The steps were taken from @liaden's post and @seanbjornsson's addition of the import example.

You can find the RailsByte here: https://railsbytes.com/templates/X6ksy1. I opted not to use the auto import example as it might have performance implications.

Usage

  1. Run rails app:template LOCATION="https://railsbytes.com/script/X6ksy1" which will add all the necessary configuration.
  2. Add any stimulus controllers to app/javascript/controllers/index.js using the example that was generated.

@reeganviljoen
Copy link
Collaborator

@maks112v could you please add it to the documentation as well, since this is the first place someone will look when they are having config problems, if you need help adding it I would be glad to help.

@reeganviljoen
Copy link
Collaborator

@maks112v hows this coming along, can I help you so we can get this issue closed

@jsntv200
Copy link

jsntv200 commented Jul 19, 2023

Ive been able to get it working with PR#192 I've submitted to importmap-rails. I currently have it running as a monkey patch working for propshaft and sprockets.

# lib/monkey_patches/importmap.rb

module MonkeyPatches
  module Importmap
    def module_path_from(filename, mapping)
      [ mapping.path || mapping.under, filename.to_s ].reject(&:empty?).join("/")
    end
  end
end
# config/initializers/monkey_patches.rb

require_relative "../../lib/monkey_patches/importmap"

Importmap::Map.prepend MonkeyPatches::Importmap
# config/initializers/assets.rb

Rails.application.config.assets.paths << "app/components"
# config/importmap.rb

pin_all_from "app/components", under: "controllers", to: ""

Propshaft - just ensure controllers are imported, should be there in default install

// app/javascript/application.js

import "controllers";

Sprockets - link the components directory

// app/assets/config/manifest.js

//= link_tree ../../components .js

Note: I haven't tested this in a production setup but running bin/importmap json returns all the correct assets with their hash digests.

@kengreeff
Copy link

Does anyone know how to automatically run stimulus:manifest:update after generating the component? Feels like it is definitely something I will forget to do and cause hours of debugging lol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests