ViewComponent / view_component

A framework for building reusable, testable & encapsulated view components in Ruby on Rails.
https://viewcomponent.org
MIT License
3.2k stars 411 forks source link

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

Open brunoprietog opened 2 years ago

brunoprietog commented 2 years ago

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 commented 2 years ago

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

liaden commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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.

wdiechmann commented 2 years ago

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

🥳

seanbjornsson commented 2 years ago

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:

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 commented 2 years ago

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.

seanbjornsson commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

@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)

tripptuttle commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

: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!

progapandist commented 2 years ago

@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 commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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: https://github.com/github/view_component/issues/1393

khash commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

jsntv200 commented 11 months ago

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 commented 5 months ago

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

paul commented 1 month ago

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

reeganviljoen commented 1 month ago

@paul just read it, its a great article, I would love to help you add this to the docs

paul commented 1 month ago

@reeganviljoen Sure, happy to contribute. Which doc should I add it to?

reeganviljoen commented 1 month ago

@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 😀