Open brunoprietog opened 2 years ago
@brunoprietog I'd be happy to add it to the documentation. Are you able to look into this?
I got this hooked up today. To do this you should:
pin_all_from "app/components"
to config/importmaps.rb
Rails.application.config.assets.paths << "app/components"
to config/initializers/assets.rb
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?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.
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 got this hooked up today. To do this you should:
- Make sure you are using this change which is in version 0.5.2.
- Add
pin_all_from "app/components"
toconfig/importmaps.rb
- Add
Rails.application.config.assets.paths << "app/components"
toconfig/initializers/assets.rb
- Add
Rails.application.config.assets.debug = true
toconfig/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?- Add
config.assets.check_precompiled_asset = false
toconfig/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
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
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);
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.
@chunlea thank you so much for sharing! I've been banging my head on this too many hours!
🥳
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:
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.
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.
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).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
//=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....
I've came up with a very dirty (IMHO) solution a while ago, but waited for someone to suggest a cleaner one :sweat_smile:
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 https://github.com/imustafin/sibrowser/commit/12fb4aecb32775243c2924ef1f2b7c30fdf045c2.
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.
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.
@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)
@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.
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?
@tripptuttle if I understood you correctly, my solution should work. In the mentioned commit https://github.com/imustafin/sibrowser/commit/12fb4aecb32775243c2924ef1f2b7c30fdf045c2 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)
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)
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.
Yeah, it all works, but it just throws an error I think while resolving the request for the controller out of the import map.
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 thestimulus:manifest:update
. Why I put this file insideapp/components/
is becausestimulus-rails
only generate the manifest content based on relative of/app/javascript/controllers/
.And then we can just simple use
import '../components';
inapp/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
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.
:wave: @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!
@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
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!
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?
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.
It gets a bit messy when using namespaced components and sidecar folders. If I use
rails g Page::DarkModeSelector
to generatePage::DarkModeSelectorComponent
I have to usedata-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: https://github.com/github/view_component/issues/1393
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
@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.
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.
rails app:template LOCATION="https://railsbytes.com/script/X6ksy1"
which will add all the necessary configuration.app/javascript/controllers/index.js
using the example that was generated.@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.
@maks112v hows this coming along, can I help you so we can get this issue closed
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.
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
For others coming across this issue, I wrote a post about what worked for me (a combination of several answers in this issue): https://blog.theamazingrando.com/posts/rails-propshaft-stimulus-viewcomponent.html
@paul just read it, its a great article, I would love to help you add this to the docs
@reeganviljoen Sure, happy to contribute. Which doc should I add it to?
@paul a good place to add it would be https://viewcomponent.org/guide/javascript_and_css.html
You can run and edit the doc site localy in the repo by following https://viewcomponent.org/CONTRIBUTING.html#documentation
If you need any more help I let me know 😀
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: