Skip to content

Commit

Permalink
refactor: generator + drop old Ruby
Browse files Browse the repository at this point in the history
  • Loading branch information
palkan committed Nov 8, 2023
1 parent 1e548dc commit caa6fb2
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 196 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ jobs:
ruby: ["3.0"]
gemfile: ["gemfiles/rails7.gemfile"]
include:
- ruby: "2.6"
gemfile: "gemfiles/rails6.gemfile"
- ruby: "3.1"
gemfile: "gemfiles/railsmaster.gemfile"
- ruby: "2.7"
Expand Down
2 changes: 1 addition & 1 deletion .rbnextrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
nextify: |
./lib
--min-version=2.6
--min-version=2.7
--edge
--proposed
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## master

- **Require Ruby 2.7+**. ([@palkan][])

- Add system tests to generator. ([@palkan][])

- Drop Webpack-related stuff from the generator. ([@palkan][])

## 0.1.6 (2023-11-07)

- Support preview classes named `<component|partial>_preview.rb`. ([@palkan][])
Expand Down
92 changes: 71 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ The command above:
- Configure `view_component` paths.
- Adds `ApplicationViewComponent` and `ApplicationViewComponentPreview` classes.
- Configures testing framework (RSpec or Minitest).
- Adds required JS/CSS configuration.
- **Adds a custom generator to create components**.

The custom generator would allow you to create all the required component files in a single command:
Expand Down Expand Up @@ -94,6 +93,8 @@ ActiveSupport.on_load(:view_component) do
end
```

You can still continue using preview clases with the `_preview.rb` suffix, they would work as before.

#### Reducing previews boilerplate

In most cases, previews contain only the `default` example and a very simple template (`= render Component.new(**options)`).
Expand Down Expand Up @@ -182,7 +183,7 @@ If you need more control over your template, you can add a custom `preview.html.

## Organizing assets (JS, CSS)

**NOTE*: This section assumes the usage of Webpack, Vite or other _frontend_ builder (e.g., not Sprockets).
**NOTE**: This section assumes the usage of Vite or Webpack. See [this discussion](https://github.com/palkan/view_component-contrib/discussions/14) for other options.

We store JS and CSS files in the same sidecar folder:

Expand All @@ -203,6 +204,18 @@ import "./index.css"

In the root of the `components` folder we have the `index.js` file, which loads all the components:

- With Vite:

```js
// With Vite
import.meta.glob("./**/index.js").forEach((path) => {
const mod = await import(path);
mod.default();
});
```

- With Webpack:

```js
// components/index.js
const context = require.context(".", true, /index.js$/)
Expand All @@ -211,12 +224,11 @@ context.keys().forEach(context);

### Using with StimulusJS

You can define Stimulus controllers right in the `index.js` file using the following approach:
You can define Stimulus controllers right in the component folder in the `controller.js` file:

```js
import "./index.css"
// We reserve Controller for the export name
import { Controller as BaseController } from "stimulus";
import { Controller as BaseController } from "@hotwired/stimulus";

export class Controller extends BaseController {
connect() {
Expand All @@ -225,20 +237,60 @@ export class Controller extends BaseController {
}
```

Then, we need to update the `components/index.js` to automatically register controllers:
Then, in your Stimulus entrypoint, you can load and register your component controllers as follows:

- With Vite:

```js
// We recommend putting Stimulus application instance into its own
// module, so you can use it for non-component controllers
import { Application } from "@hotwired/stimulus";

const application = Application.start();

// Configure Stimulus development experience
application.debug = false;
window.Stimulus = application;

// Generic controllers
const genericControllers = import.meta.globEager(
"../controllers/**/*_controller.js"
);

for (let path in genericControllers) {
let module = genericControllers[path];
let name = path
.match(/controllers\/(.+)_controller\.js$/)[1]
.replaceAll("/", "-")
.replaceAll("_", "-");

// init/stimulus.js
application.register(name, module.default);
}

// Controllers from components
const controllers = import.meta.globEager(
"./../../app/frontend/components/**/controller.js"
);

for (let path in controllers) {
let module = controllers[path];
let name = path
.match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1]
.replaceAll("/", "-")
.replaceAll("_", "-");
application.register(name, module.default);
}

export default application;
```

- With Webpack:

```js
import { Application } from "stimulus";
export const application = Application.start();

// components/index.js
import { application } from "../init/stimulus";
// ... other controllers

const context = require.context(".", true, /index.js$/)
const context = require.context("./../../app/frontend/components/", true, /controllers.js$/)
context.keys().forEach((path) => {
const mod = context(path);

Expand All @@ -248,9 +300,10 @@ context.keys().forEach((path) => {
// Convert path into a controller identifier:
// example/index.js -> example
// nav/user_info/index.js -> nav--user-info
const identifier = path.replace(/^\.\//, '')
.replace(/\/index\.js$/, '')
.replace(/\//g, '--');
const identifier = path
.match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1]
.replaceAll("/", "-")
.replaceAll("_", "-");

application.register(identifier, mod.Controller);
});
Expand All @@ -265,14 +318,16 @@ class ApplicationViewComponent
def identifier
@identifier ||= self.class.name.sub("::Component", "").underscore.split("/").join("--")
end

alias_method :controller_name, :identifier
end
```

And now in your template:

```erb
<!-- component.html -->
<div data-controller="<%= identifier %>">
<div data-controller="<%= controller_name %>">
</div>
```

Expand Down Expand Up @@ -495,11 +550,6 @@ And the template looks like this now:

You can use the `#wrapped` method on any component inherited from `ApplicationViewComponent` to wrap it automatically:

## ToDo list

- Better preview tools (w/o JS deps 😉).
- Hotwire-related extensions.

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
Expand Down
3 changes: 0 additions & 3 deletions templates/install/class_for.rb

This file was deleted.

115 changes: 41 additions & 74 deletions templates/install/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ViewComponentGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
class_option :skip_test, type: :boolean, default: false
class_option :skip_system_test, type: :boolean, default: false
class_option :skip_preview, type: :boolean, default: false
argument :attributes, type: :array, default: [], banner: "attribute"
Expand All @@ -32,6 +33,12 @@ def create_test_file
template "component_#{TEST_SUFFIX}.rb", File.join("#{TEST_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb")
end
def create_system_test_file
return if options[:skip_system_test]
template "component_system_#{TEST_SUFFIX}.rb", File.join("#{TEST_SYSTEM_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb")
end
def create_preview_file
return if options[:skip_preview]
Expand All @@ -50,31 +57,6 @@ def preview_parent_class
end
CODE

if USE_WEBPACK
inject_into_file "lib/generators/view_component/view_component_generator.rb", after: "class_option :skip_preview, type: :boolean, default: false\n" do
<<-CODE
class_option :skip_js, type: :boolean, default: false
class_option :skip_css, type: :boolean, default: false
CODE
end

inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\n private" do
<<-CODE
def create_css_file
return if options[:skip_css] || options[:skip_js]
template "index.css", File.join("#{ROOT_PATH}", class_path, file_name, "index.css")
end
def create_js_file
return if options[:skip_js]
template "index.js", File.join("#{ROOT_PATH}", class_path, file_name, "index.js")
end
CODE
end
end

if USE_DRY
inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\nend" do
<<-CODE
Expand Down Expand Up @@ -167,43 +149,6 @@ def default
end
CODE

if USE_WEBPACK
if USE_STIMULUS
file "lib/generators/view_component/templates/index.js.tt",
<<-CODE
import "./index.css"
// Add a Stimulus controller for this component.
// It will automatically registered and its name will be available
// via #component_name in the component class.
//
// import { Controller as BaseController } from "stimulus";
//
// export class Controller extends BaseController {
// connect() {
// }
//
// disconnect() {
// }
// }
CODE
else
file "lib/generators/view_component/templates/index.js.tt", <<~CODE
import "./index.css"
CODE
end

if USE_POSTCSS_MODULES
file "lib/generators/view_component/templates/index.css.tt", <<~CODE
/* Use component-local class names and add them to HTML via #class_for(name) helper */
CODE
else
file "lib/generators/view_component/templates/index.css.tt", ""
end
end

if USE_RSPEC
file "lib/generators/view_component/templates/component_spec.rb.tt", <<~CODE
# frozen_string_literal: true
Expand All @@ -223,15 +168,29 @@ def default
end
end
CODE

file "lib/generators/view_component/templates/component_system_spec.rb.tt", <<~CODE
# frozen_string_literal: true
require "rails_helper"
describe "<%%= file_name %> component" do
it "default preview" do
visit("/rails/view_components<%%= File.join(class_path, file_name) %>/default")
# is_expected.to have_text "Hello!"
# click_on "Click me"
# is_expected.to have_text "Good-bye!"
end
end
CODE
else
file "lib/generators/view_component/templates/component_test.rb.tt", <<~CODE
# frozen_string_literal: true
require "test_helper"
class <%%= class_name %>::ComponentTest < ActiveSupport::TestCase
include ViewComponent::TestHelpers
class <%%= class_name %>::ComponentTest < ViewComponent::TestCase
def test_renders
component = build_component
Expand All @@ -245,6 +204,22 @@ def test_renders
def build_component(**options)
<%%= class_name %>::Component.new(**options)
end
end
CODE

file "lib/generators/view_component/templates/component_system_test.rb.tt", <<~CODE
# frozen_string_literal: true
require "application_system_test_case"
class <%%= class_name %>::ComponentSystemTest < ApplicationSystemTestCase
def test_default_preview
visit("/rails/view_components<%%= File.join(class_path, file_name) %>/default")
# assert_text "Hello!"
# click_on("Click me!")
# assert_text "Good-bye!"
end
end
CODE
end
Expand All @@ -263,18 +238,10 @@ def build_component(**options)
Component: #{ROOT_PATH}/profile/component.rb
Template: #{ROOT_PATH}/profile/component.html#{TEMPLATE_EXT}
Test: #{TEST_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb
System Test: #{TEST_SYSTEM_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb
Preview: #{ROOT_PATH}/profile/component_preview.rb
CODE

if USE_WEBPACK
inject_into_file "lib/generators/view_component/USAGE" do
<<-CODE
JS: #{ROOT_PATH}/profile/component.js
CSS: #{ROOT_PATH}/profile/component.css
CODE
end
end

# Check if autoload_lib is configured
if File.file?("config/application.rb") && File.read("config/application.rb").include?("config.autoload_lib")
say_status :info, "⚠️ Make sure you configured autoload_lib to ignore the lib/generators folder"
Expand Down
2 changes: 0 additions & 2 deletions templates/install/index.js

This file was deleted.

Loading

0 comments on commit caa6fb2

Please sign in to comment.