├── .github └── workflows │ └── main.yml ├── .gitignore ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app └── helpers │ └── shoelace │ ├── components │ ├── error_wrappable.rb │ ├── sl_checkbox.rb │ ├── sl_collection_radio_buttons.rb │ ├── sl_color_picker.rb │ ├── sl_input.rb │ ├── sl_radio_button.rb │ ├── sl_range.rb │ ├── sl_select.rb │ ├── sl_switch.rb │ └── sl_textarea.rb │ ├── form_builder.rb │ ├── sl_form_builder.rb │ ├── sl_form_helper.rb │ └── tag_helper.rb ├── bin ├── console └── setup ├── gemfiles ├── rails_50.gemfile ├── rails_51.gemfile ├── rails_52.gemfile ├── rails_60.gemfile ├── rails_61.gemfile ├── rails_70.gemfile ├── rails_71.gemfile ├── rails_72.gemfile └── rails_edge.gemfile ├── lib ├── shoelace │ ├── engine.rb │ ├── rails.rb │ ├── rails │ │ └── version.rb │ ├── railtie.rb │ └── testing.rb └── tasks │ └── shoelace.rake ├── shoelace-rails.gemspec └── test ├── helpers ├── form_builder │ ├── sl_checkbox_test.rb │ ├── sl_color_picker_test.rb │ ├── sl_input_test.rb │ ├── sl_radio_group_test.rb │ ├── sl_range_test.rb │ ├── sl_select_test.rb │ ├── sl_switch_test.rb │ └── sl_textarea_test.rb ├── form_builder_test.rb ├── form_helper_test.rb ├── tag_helper_test.rb └── translation_test.rb └── test_helper.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | unit: 9 | strategy: 10 | matrix: 11 | ruby_version: 12 | - '3.3' 13 | - '3.2' 14 | - '3.1' 15 | - '3.0' 16 | - '2.7' 17 | - 'jruby-9.3' 18 | - 'jruby-9.4' 19 | gemfile: 20 | - gemfiles/rails_71.gemfile 21 | - gemfiles/rails_70.gemfile 22 | - gemfiles/rails_61.gemfile 23 | - gemfiles/rails_60.gemfile 24 | exclude: 25 | - ruby_version: 'jruby-9.3' 26 | gemfile: gemfiles/rails_71.gemfile 27 | - ruby_version: 'jruby-9.3' 28 | gemfile: gemfiles/rails_70.gemfile 29 | runs-on: ubuntu-22.04 30 | env: 31 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Ruby 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: ${{ matrix.ruby_version }} 38 | bundler-cache: true 39 | - run: bundle exec rake test 40 | 41 | rails_edge: 42 | needs: unit 43 | runs-on: ubuntu-22.04 44 | env: 45 | BUNDLE_GEMFILE: gemfiles/rails_edge.gemfile 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Set up Ruby 49 | uses: ruby/setup-ruby@v1 50 | with: 51 | ruby-version: 3.3 52 | bundler-cache: true 53 | - run: bundle exec rake test || echo "Rails edge test is done." 54 | 55 | ruby_edge: 56 | needs: unit 57 | strategy: 58 | matrix: 59 | gemfile: 60 | - gemfiles/rails_edge.gemfile 61 | - gemfiles/rails_71.gemfile 62 | runs-on: ubuntu-22.04 63 | env: 64 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 65 | steps: 66 | - uses: actions/checkout@v4 67 | - name: Set up Ruby 68 | uses: ruby/setup-ruby@v1 69 | with: 70 | ruby-version: 'ruby-head' 71 | bundler-cache: true 72 | - run: bundle exec rake || echo "Ruby edge test is done." 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | .npmrc 11 | 12 | .pryrc 13 | gemfiles/*.lock 14 | gemfiles/.bundle 15 | BrowserStackLocal 16 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails_edge" do 2 | git "https://github.com/rails/rails.git", branch: "main" do 3 | gem "rails" 4 | gem "railties" 5 | gem "activesupport" 6 | end 7 | end 8 | 9 | appraise "rails_72" do 10 | gem "rails", "~> 7.2.0" 11 | gem "railties", "~> 7.2.0" 12 | gem "activesupport", "~> 7.2.0" 13 | end 14 | 15 | appraise "rails_71" do 16 | gem "rails", "~> 7.1.0" 17 | gem "railties", "~> 7.1.0" 18 | gem "activesupport", "~> 7.1.0" 19 | end 20 | 21 | appraise "rails_70" do 22 | gem "rails", "~> 7.0.0" 23 | gem "railties", "~> 7.0.0" 24 | gem "activesupport", "~> 7.0.0" 25 | end 26 | 27 | appraise "rails_61" do 28 | gem "rails", '~> 6.1.0' 29 | gem "railties", '~> 6.1.0' 30 | gem "activesupport", '~> 6.1.0' 31 | end 32 | 33 | appraise "rails_60" do 34 | gem "rails", '~> 6.0.0' 35 | gem "railties", '~> 6.0.0' 36 | gem "activesupport", '~> 6.0.0' 37 | end 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | #### 🚨 Breaking Changes 4 | 5 | #### ⭐️ Features 6 | 7 | #### 🐞 Bug Fixes 8 | 9 | ## [v0.8.0](https://github.com/yuki24/shoelace-rails/tree/v0.8.0) 10 | 11 | _released at 2024-08-16 05:46:10 UTC_ 12 | 13 | #### ⭐️ Features 14 | 15 | - Add support for Rails 7.1 ([5f934f1](https://github.com/yuki24/shoelace-rails/commit/5f934f10660dd79a5317205536edf7d18df2bb97)) 16 | 17 | ## [v0.7.0](https://github.com/yuki24/shoelace-rails/tree/v0.7.0) 18 | 19 | _released at 2024-08-01 04:47:41 UTC_ 20 | 21 | #### 🚨 Breaking Changes 22 | 23 | - Deprecate `config.shoelace.invalid_input_class_name` in favor of `data-invalid` and `data-use-invalid` ([461b622](https://github.com/yuki24/shoelace-rails/commit/461b6229a3b1917fab6db49ddc9f10003b8a54f9)) 24 | 25 | #### ⭐️ Features 26 | 27 | - Add `invalid` and `data-invalid` to the `Shoelace::FormBuilder` ([461b622](https://github.com/yuki24/shoelace-rails/commit/461b6229a3b1917fab6db49ddc9f10003b8a54f9)) 28 | - Add the ability to specify a method for rendering slot for input components ([6b8b2d3](https://github.com/yuki24/shoelace-rails/commit/6b8b2d3537bea4fc779b25183be6b8b4ad6b8365)) 29 | - Add `FormBuilder#date_field` method ([73b92be](https://github.com/yuki24/shoelace-rails/commit/73b92becbf44bc277202e4085201c035fed430bb)) 30 | 31 | #### 🐞 Bug Fixes 32 | 33 | - Fixes a bug where `FormBuilder#select` blows up with an empty block ([f0addb2](https://github.com/yuki24/shoelace-rails/commit/f0addb2315f03daa0a11a9c4227e427c0666cd3f)) 34 | 35 | ## [v0.6.2](https://github.com/yuki24/shoelace-rails/tree/v0.6.2) 36 | 37 | _released at 2024-08-01 04:38:17 UTC_ 38 | 39 | #### 🐞Bug Fixes 40 | 41 | - Fixes a bug where form builders fail to render when it falls back to humanize the given method name ([74b646e](https://github.com/yuki24/shoelace-rails/commit/74b646e3fa96768680dd1fda314b8367f98ee69a)) 42 | 43 | ## [v0.6.1](https://github.com/yuki24/shoelace-rails/tree/v0.6.1) 44 | 45 | _released at 2024-03-13 03:05:20 UTC_ 46 | 47 | #### 🐞Bug Fixes 48 | 49 | - Fixes a bug where form builders fail to render with a string `:as` option ([d631025](https://github.com/yuki24/shoelace-rails/commit/d63102559fdcaaa79c01a210769667cac77b197d)) 50 | 51 | ## [v0.6.0](https://github.com/yuki24/shoelace-rails/tree/v0.6.0) 52 | 53 | _released at 2024-03-13 02:54:02 UTC_ 54 | 55 | #### ⭐️ Features 56 | 57 | - Add the ability to use translations with form helpers ([626f271](https://github.com/yuki24/shoelace-rails/commit/626f271ca710dd48040907ff6a99e3bba6c5d57c)) 58 | 59 | ## [v0.5.0](https://github.com/yuki24/shoelace-rails/tree/v0.5.0) 60 | 61 | _released at 2024-03-09 08:54:30 UTC_ 62 | 63 | #### ⭐️ Features 64 | 65 | - Add support for Ruby 3.3 ([399f255](https://github.com/yuki24/shoelace-rails/commit/399f25567f964d0ea2e250eba6db28a2bcd038a3)) 66 | - Add support for Rails 7.1 ([#4](https://github.com/yuki24/shoelace-rails/pull/4)) 67 | - Add `#grouped_collection_select` ([2b91023](https://github.com/yuki24/shoelace-rails/commit/2b91023d51e1d0a218f2102232241afa82aaf872)) 68 | - Make the `` form helpers compatible with Shoelace [2.0.0-beta.80](https://shoelace.style/resources/changelog#id_2_0_0-beta_80) and above ([ef9a834](https://github.com/yuki24/shoelace-rails/commit/ef9a8345f2c5c921847aef15e19cf64a471d6473)) 69 | 70 | ## [v0.4.1](https://github.com/yuki24/shoelace-rails/tree/v0.4.1) 71 | 72 | _released at 2023-03-21 04:03:27 UTC_ 73 | 74 | #### 🐞Bug Fixes 75 | 76 | - Fixes a bug where `FormHelper` may not be defined when someone loads `ActionView` too early ([d91ed3b](https://github.com/yuki24/shoelace-rails/commit/d91ed3b595c01ce2dfc471b12b14311e0660d3d7)) 77 | - Fixes a bug where the Shoelace rake tasks blow up when the project does not depend on Sprockets or Propshaft ([0e64cd6](https://github.com/yuki24/shoelace-rails/commit/0e64cd6dc38a037171be04eaf1d3f59c3c8529eb), [75adf83](https://github.com/yuki24/shoelace-rails/commit/75adf831b1faa7f5d1faeed26e672d4bc89b9513)) 78 | 79 | ## [v0.4.0](https://github.com/yuki24/shoelace-rails/tree/v0.4.0) 80 | 81 | _released at 2023-01-07 07:23:50 UTC_ 82 | 83 | #### 🚨 Breaking Changes 84 | 85 | - No longer works with `2.0.0-beta.87` and below. 86 | 87 | #### ⭐️ Features 88 | 89 | - Support Shoelace.style `2.0.0-beta.88`. 90 | 91 | ## [v0.3.0](https://github.com/yuki24/shoelace-rails/tree/v0.3.0) 92 | 93 | _released at 2023-01-05 08:49:23 UTC_ 94 | 95 | #### Features 96 | 97 | - No longer requires the `sl-form` component ([4fdbfa1](https://github.com/yuki24/shoelace-rails/commit/4fdbfa15fa10db9e7240378ca34ebcc494d18f1a)) 98 | - The `#text_area` method now accepts a block ([5092dc1](https://github.com/yuki24/shoelace-rails/commit/5092dc1cbc7e8e74552451450804baa378ab1f11)) 99 | - Allow overriding the value attribute for `` ([1f38be7](https://github.com/yuki24/shoelace-rails/commit/1f38be73e3335c10e846393ebcf5155d155b00b2)) 100 | - Auto-display labels whenever possible ([c1e3a95](https://github.com/yuki24/shoelace-rails/commit/c1e3a950c3e8ac4238ed3e83e4d87467a68eb91f)) 101 | - `` now always has a label by default ([f9fb5f0](https://github.com/yuki24/shoelace-rails/commit/f9fb5f0cd74d179241be51510fa1c306481946c9)) 102 | - Support Ruby 3.2 ([b286cbc](https://github.com/yuki24/shoelace-rails/commit/b286cbc18930218ab5c82bd8648a51e9c6ce53db)) 103 | - Add `#sl_button_to` ([e1bdedb](https://github.com/yuki24/shoelace-rails/commit/e1bdedba4656d89a82c78641644490085da1fa37)) 104 | - Add `#sl_icon_tag` ([8a2187a](https://github.com/yuki24/shoelace-rails/commit/8a2187a2800771512fccf2c8231378a77be59df4)) 105 | - Add `#sl_avatar_tag` ([77dccdb](https://github.com/yuki24/shoelace-rails/commit/77dccdb24cfc014bd997ffb66ad89ff95afb3ef7)) 106 | - Allow using the `Shoelace::FormBuilder` in a cleaner way ([43dea33](https://github.com/yuki24/shoelace-rails/commit/43dea3309c3e0cf9d9b43b6957f6e54ad9497c9f)) 107 | 108 | #### Bug Fixes 109 | 110 | - Fixes a bug where the gem rake tasks are not loaded ([115bfb3](https://github.com/yuki24/shoelace-rails/commit/115bfb3d81ca19b5b922a5fb32f46adb1d6e8544)) 111 | - Fixes a bug where values are not properly passed in to `` ([3d16384](https://github.com/yuki24/shoelace-rails/commit/3d16384554bd4a6143e28e483f8d6bee8fb2e073)) 112 | - Make sure yarn install is always executed before copying shoelace assets ([98018a2](https://github.com/yuki24/shoelace-rails/commit/98018a27a29ddc9ff2c2fa066bbe986709803a1d)) 113 | - Fixes a bug where the `@object` needs to respond to `#errors` ([bb981ed](https://github.com/yuki24/shoelace-rails/commit/bb981ed05825707cef89d70a7d1699c12cd0ba9b)) 114 | - Fixes a bug where the `size` attr is ignored by the `#text_area` method ([8bc4c37](https://github.com/yuki24/shoelace-rails/commit/8bc4c3784a458e7fc9c18a143578b2cbf588e9e7)) 115 | - Fixes a bug where unchecked checkbox values are not captured ([dc658be](https://github.com/yuki24/shoelace-rails/commit/dc658bea9fc4d4205dacdfe133b091c5a5edf14c)) 116 | 117 | ## [v0.2.0](https://github.com/yuki24/shoelace-rails/tree/v0.2.0) 118 | 119 | _released at 2022-06-24 05:14:01 UTC_ 120 | 121 | #### Features 122 | 123 | - Do not require the `copy-webpack-plugin` to set up Shoelace so the gem works with any js bundler. 124 | 125 | ## [v0.1.0](https://github.com/yuki24/shoelace-rails/tree/v0.1.0) 126 | 127 | _released at 2022-02-17 13:17:09 UTC_ 128 | 129 | First release! 130 | 131 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at yuki24@hey.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in shoelace-rails.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yuki Nishijima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shoelace Rails 2 | 3 | The `shoelace-rails` gem adds useful helper methods for [Shoelace.style](https://shoelace.style), the design system. 4 | 5 | ## Compatibility with Shoelace 6 | 7 | * **For Shoelace version >= 2.0.0-beta.88, use the gem version `0.4.0` or above**. 8 | * For Shoelace version <= 2.0.0-beta.87, use the gem version `0.3.0`. 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'shoelace-rails' 16 | ``` 17 | 18 | And then execute: 19 | 20 | ``` 21 | $ bundle install 22 | ``` 23 | 24 | Additionally, you need to add the following npm packages: 25 | 26 | ```sh 27 | $ yarn add @shoelace-style/shoelace 28 | ``` 29 | 30 | ## Set up CSS 31 | 32 | ### Asset Pipeline 33 | 34 | Add the following lines to the `app/assets/stylesheets/application.css` if you need the sprockets gem: 35 | 36 | ```scss 37 | /* 38 | *= require @shoelace-style/shoelace/dist/themes/light.css 39 | *= require @shoelace-style/shoelace/dist/themes/dark.css 40 | */ 41 | ``` 42 | 43 | ### Webpack & CSS Loader 44 | 45 | Add the following lines to the `app/packs/entrypoints/application.js` if you prefer the webpack and CSS loader: 46 | 47 | ```js 48 | import "@shoelace-style/shoelace/dist/themes/light.css" 49 | import "@shoelace-style/shoelace/dist/themes/dark.css" // Optional dark mode 50 | ``` 51 | 52 | ## Set up Javascript 53 | 54 | In this README, it is assumed that you are using a JS bundler such as `webpack` or `esbuild`. In order to define all 55 | the custome elements, import the shoelace dependency in the entrypoint file: 56 | 57 | ```js 58 | import "@shoelace-style/shoelace" 59 | ``` 60 | 61 | That's it! 62 | 63 | ### Shoelace Icons 64 | 65 | Shoelace icons are automatically set up to load properly, so you don't need to add any extra code. More specifically, 66 | 67 | * In development, the icons are served by the `ActionDispatch::Static` middleware, directly from the 68 | `node_modules/@shoelace-style/shoelace/dist/assets/icons` directory. 69 | * In production, the icon files are automatically copied into the `public/assets` directory as part of the 70 | `assets:precompile` rake task. 71 | 72 | ## View Helpers 73 | 74 | As explained above, this gem provides drop-in replacements to Rails view helpers. 75 | 76 | ### Form Helpers 77 | 78 | The `sl_form_with` or `sl_form_for` method could be used to generate a form with the Shoelace components: 79 | 80 | ```erb 81 | <%= sl_form_for @user do |form| %> 82 | <% # Text input: https://shoelace.style/components/input %> 83 | <%= form.text_field :name %> 84 | <%= form.password_field :password, placeholder: "Password Toggle", 'toggle-password': true %> 85 | 86 | <% # Radio buttons: https://shoelace.style/components/color-picker %> 87 | <%= form.color_field :color %> 88 | 89 | <% # Radio buttons: https://shoelace.style/components/radio %> 90 | <%= form.collection_radio_buttons :status, { id_1: "Option 1", id_2: "Option 2", id_3: "Option 3" }, :first, :last %> 91 | 92 | <% # Select: https://shoelace.style/components/select %> 93 | <%= form.collection_select :tag, { id_1: "Option 1", id_2: "Option 2", id_3: "Option 3" }, :first, :last, {}, { placeholder: "Select one" } %> 94 | 95 | <%= form.submit %> 96 | <% end %> 97 | ``` 98 | 99 | And this code will produce: 100 | 101 | ```html 102 |
103 | 104 | 105 | 106 | 107 | 108 | Option 1 109 | Option 2 110 | Option 3 111 | 112 | 113 | 114 | Option 1 115 | Option 2 116 | Option 3 117 | 118 | 119 | Create User 120 |
121 | ``` 122 | 123 | #### Using The `sl-select` component with `multiple` 124 | 125 | TDB 126 | 127 | #### Using the Shoelace FormBuilder with other gems 128 | 129 | Sometimes you want to use the Shoelace FormBuilder with other gems, such as [ransack](https://github.com/activerecord-hackery/ransack). 130 | In this case, you can not use the `sl_form_for` or `sl_form_with` methods in tandem with `ransack`, but you can use 131 | the `Shoelace::FormBuilder` with e.g. [the `search_form_for` method](https://activerecord-hackery.github.io/ransack/getting-started/simple-mode/#form-helper): 132 | 133 | ```erb 134 | <%= search_form_for @q, builder: Shoelace::FormBuilder do |form| %> 135 | ... 136 | <% end %> 137 | ``` 138 | 139 | ### Tag Helpers 140 | 141 | #### `#sl_avatar_tag` 142 | 143 | The `@sl_avatar_tag` method behaves just like the `image_tag` method. 144 | 145 | ```erb 146 | <%= sl_avatar_tag "/path/to/image.jpg" %> 147 | ``` 148 | 149 | Will produce: 150 | 151 | ```html 152 | 153 | ``` 154 | 155 | #### `#sl_button_to` 156 | 157 | The `sl_button_to` method behaves just like the `link_to` method. Note that this is slightly different from the 158 | built-in `button_to` method. 159 | 160 | Without a block: 161 | 162 | ```erb 163 | <%= sl_button_to "Next Page", "/components?page=2" %> 164 | ``` 165 | 166 | ```html 167 | 168 | Next Page 169 | 170 | ``` 171 | 172 | With a block: 173 | 174 | ```erb 175 | <%= sl_button_to "/components?page=2" do %> 176 | Next Page 177 | <% end %> 178 | ``` 179 | 180 | ```html 181 | 182 | Next Page 183 | 184 | ``` 185 | 186 | #### `#sl_icon_tag` 187 | 188 | The `sl_icon_tag` method takes the `name` attribute value as the first argument: 189 | 190 | ```erb 191 | <%= sl_icon_tag "apple" %> 192 | ``` 193 | 194 | ```html 195 | 196 | ``` 197 | 198 | ## Development 199 | 200 | 1. Run `bundle install` 201 | 2. Make a change and add test coverage 202 | 3. Run `bundle rails test` 203 | 4. Make a commit and push it to GitHub 204 | 5. Send us a pull request 205 | 206 | ## Contributing 207 | 208 | Bug reports and pull requests are welcome on GitHub at https://github.com/yuki24/shoelace-rails. This project is 209 | intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the 210 | [code of conduct](https://github.com/yuki24/shoelace-rails/blob/master/CODE_OF_CONDUCT.md). 211 | 212 | ## License 213 | 214 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 215 | 216 | ## Code of Conduct 217 | 218 | Everyone interacting in the Shoelace::Rails project's codebases, issue trackers, chat rooms and mailing lists is 219 | expected to follow the [code of conduct](https://github.com/yuki24/shoelace-rails/blob/master/CODE_OF_CONDUCT.md). 220 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/*_test.rb"] 10 | end 11 | 12 | task default: [:test] 13 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/error_wrappable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Shoelace 4 | module Components 5 | module ErrorWrappable 6 | def error_wrapping(html_tag) 7 | if object_has_errors? && field_error_proc 8 | @template_object.instance_exec(html_tag, self, &field_error_proc) 9 | else 10 | html_tag 11 | end 12 | end 13 | 14 | private 15 | 16 | def field_error_proc 17 | Shoelace::SlFormBuilder.field_error_proc 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_checkbox.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlCheckbox < ActionView::Helpers::Tags::CheckBox #:nodoc: 8 | include ErrorWrappable 9 | 10 | def render(&block) 11 | options = @options.stringify_keys 12 | options["value"] = @checked_value 13 | options["checked"] = true if input_checked?(options) 14 | options["invalid"] = options["data-invalid"] = "" if object_has_errors? 15 | label = options.delete("label") 16 | 17 | if options["multiple"] 18 | add_default_name_and_id_for_value(@checked_value, options) 19 | options.delete("multiple") 20 | else 21 | add_default_name_and_id(options) 22 | end 23 | 24 | include_hidden = options.delete("include_hidden") { true } 25 | 26 | sl_checkbox_tag = if block_given? 27 | @template_object.content_tag('sl-checkbox', '', options, &block) 28 | else 29 | @template_object.content_tag('sl-checkbox', label, options) 30 | end 31 | 32 | if include_hidden 33 | hidden_field_for_checkbox(options) + sl_checkbox_tag 34 | else 35 | sl_checkbox_tag 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_collection_radio_buttons.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlCollectionRadioButtons < ActionView::Helpers::Tags::CollectionRadioButtons #:nodoc: 8 | include ErrorWrappable 9 | 10 | class RadioButtonBuilder < Builder # :nodoc: 11 | def label(*) 12 | text 13 | end 14 | 15 | def radio_button(extra_html_options = {}, &block) 16 | html_options = extra_html_options.merge(@input_html_options) 17 | html_options[:skip_default_ids] = false 18 | @template_object.sl_radio_button(@object_name, @method_name, @value, html_options, &block) 19 | end 20 | end 21 | 22 | def render(&block) 23 | render_collection_for(RadioButtonBuilder, &block) 24 | end 25 | 26 | private 27 | 28 | def render_collection(&block) 29 | html_options = @html_options.stringify_keys 30 | html_options["value"] = value 31 | add_default_name_and_id(html_options) 32 | html_options["label"] = @options[:label].presence 33 | html_options["invalid"] = html_options["data-invalid"] = "" if object_has_errors? 34 | 35 | @template_object.content_tag('sl-radio-group', html_options) { super(&block) } 36 | end 37 | 38 | def hidden_field 39 | ''.html_safe 40 | end 41 | 42 | def render_component(builder) 43 | builder.radio_button { builder.label } 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_color_picker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlColorPicker < ActionView::Helpers::Tags::ColorField #:nodoc: 8 | RGB_VALUE_REGEX = /#[0-9a-fA-F]{6}/.freeze 9 | 10 | include ErrorWrappable 11 | 12 | def field_type; nil; end 13 | 14 | def tag(tag_name, *args, &block) 15 | tag_name.to_s == 'input' ? content_tag('sl-color-picker', '', *args, &block) : super 16 | end 17 | 18 | def render 19 | @options["invalid"] = @options["data-invalid"] = "" if object_has_errors? 20 | super 21 | end 22 | 23 | private 24 | 25 | def validate_color_string(string) 26 | string.downcase if RGB_VALUE_REGEX.match?(string) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlInput < ActionView::Helpers::Tags::TextField #:nodoc: 8 | include ErrorWrappable 9 | 10 | attr_reader :field_type 11 | 12 | def initialize(field_type, *args) 13 | super(*args) 14 | @field_type = field_type 15 | end 16 | 17 | def render(&block) 18 | options = @options.stringify_keys 19 | 20 | value = options.fetch("value") { value_before_type_cast } 21 | options["value"] = value if value.present? 22 | 23 | options["size"] = options["maxlength"] unless options.key?("size") 24 | options["type"] ||= field_type 25 | options["invalid"] = options["data-invalid"] = "" if object_has_errors? 26 | 27 | add_default_name_and_id(options) 28 | 29 | @template_object.content_tag('sl-input', '', options, &block) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_radio_button.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlRadioButton < ActionView::Helpers::Tags::RadioButton #:nodoc: 8 | include ErrorWrappable 9 | 10 | def render(&block) 11 | options = @options.stringify_keys 12 | options["value"] = @tag_value 13 | add_default_name_and_id_for_value(@tag_value, options) 14 | options.delete("name") 15 | 16 | @template_object.content_tag('sl-radio', '', options.except("type"), &block) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_range.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlRange < ActionView::Helpers::Tags::NumberField #:nodoc: 8 | include ErrorWrappable 9 | 10 | def field_type; nil; end 11 | 12 | def render 13 | @options["invalid"] = @options["data-invalid"] = "" if object_has_errors? 14 | super 15 | end 16 | 17 | def tag(tag_name, *args, &block) 18 | tag_name.to_s == 'input' ? content_tag('sl-range', '', *args, &block) : super 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_select.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | module SlSelectRenderable 8 | EMPTY = "".html_safe.freeze 9 | 10 | def select_content_tag(option_tags, _options, html_options) 11 | html_options = html_options.stringify_keys 12 | html_options['value'] ||= value 13 | html_options["invalid"] = html_options["data-invalid"] = "" if object_has_errors? 14 | add_default_name_and_id(html_options) 15 | 16 | @template_object.content_tag("sl-select", html_options) do 17 | "".html_safe.tap do |html| 18 | html.safe_concat(option_tags) 19 | html.safe_concat(@template_object.capture(&slot) || EMPTY) if slot 20 | end 21 | end 22 | end 23 | end 24 | 25 | class SlSelect < ActionView::Helpers::Tags::Select #:nodoc: 26 | include ErrorWrappable 27 | include SlSelectRenderable 28 | 29 | attr_reader :slot 30 | 31 | def initialize(object_name, method_name, template_object, choices, options, html_options, slot) 32 | @slot = slot 33 | super(object_name, method_name, template_object, choices, options, html_options) 34 | end 35 | 36 | def grouped_options_for_select(grouped_options, options) 37 | @template_object.grouped_sl_options_for_select(grouped_options, options) 38 | end 39 | 40 | def options_for_select(container, options = nil) 41 | @template_object.sl_options_for_select(container, options) 42 | end 43 | end 44 | 45 | class SlCollectionSelect < ActionView::Helpers::Tags::CollectionSelect #:nodoc: 46 | include ErrorWrappable 47 | include SlSelectRenderable 48 | 49 | attr_reader :slot 50 | 51 | def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options, slot) 52 | @slot = slot 53 | super(object_name, method_name, template_object, collection, value_method, text_method, options, html_options) 54 | end 55 | 56 | def options_from_collection_for_select(collection, value_method, text_method, selected = nil) 57 | @template_object.sl_options_from_collection_for_select(collection, value_method, text_method, selected) 58 | end 59 | end 60 | 61 | class SlGroupedCollectionSelect < ActionView::Helpers::Tags::GroupedCollectionSelect #:nodoc: 62 | include ErrorWrappable 63 | include SlSelectRenderable 64 | 65 | attr_reader :slot 66 | 67 | def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options, slot) 68 | @slot = slot 69 | super(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options) 70 | end 71 | 72 | def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) 73 | @template_object.sl_option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_switch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlSwitch < ActionView::Helpers::Tags::NumberField #:nodoc: 8 | include ErrorWrappable 9 | 10 | def field_type; nil; end 11 | 12 | def render(&block) 13 | options = @options.stringify_keys 14 | options["value"] = options.fetch("value") { value_before_type_cast } 15 | add_default_name_and_id(options) 16 | label = options.delete('label').presence 17 | options["invalid"] = options["data-invalid"] = "" if object_has_errors? 18 | 19 | content_tag('sl-switch', label, options, &block) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/helpers/shoelace/components/sl_textarea.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './error_wrappable' 4 | 5 | module Shoelace 6 | module Components 7 | class SlTextarea < ActionView::Helpers::Tags::NumberField #:nodoc: 8 | include ErrorWrappable 9 | 10 | def render(&block) 11 | options = @options.stringify_keys 12 | options["value"] = options.fetch("value") { value_before_type_cast } 13 | options["invalid"] = options["data-invalid"] = "" if object_has_errors? 14 | add_default_name_and_id(options) 15 | 16 | @template_object.content_tag("sl-textarea", '', options, &block) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/helpers/shoelace/form_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './sl_form_builder' 4 | 5 | module Shoelace 6 | FormBuilder = SlFormBuilder 7 | deprecate_constant :FormBuilder 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/shoelace/sl_form_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './components/sl_checkbox' 4 | require_relative './components/sl_collection_radio_buttons' 5 | require_relative './components/sl_color_picker' 6 | require_relative './components/sl_input' 7 | require_relative './components/sl_radio_button' 8 | require_relative './components/sl_range' 9 | require_relative './components/sl_select' 10 | require_relative './components/sl_switch' 11 | require_relative './components/sl_textarea' 12 | 13 | module Shoelace 14 | class SlFormBuilder < ActionView::Helpers::FormBuilder #:nodoc: 15 | # Same concept as the Rails field_error_proc, but for Shoelace components. Set to `nil` to avoid messing up the 16 | # Shoelace components by default. 17 | cattr_accessor :field_error_proc 18 | 19 | # Similar concept to the Rails field_error_proc, but for Shoelace's commonly used `help-text slot`. 20 | cattr_accessor :default_input_slot_method 21 | 22 | { 23 | date: :date, 24 | email: :email, 25 | number: :number, 26 | password: :password, 27 | search: :search, 28 | telephone: :tel, 29 | phone: :tel, 30 | text: :text, 31 | url: :url 32 | }.each do |field_type, field_class| 33 | # def email_field(method, **options, &block) 34 | # slot = wrap_with_default_slot(method, &block) 35 | # Components::SlInput.new(:email, object_name, method, @template, options.with_defaults(label: label_text(method))).render(&block) 36 | # end 37 | eval <<-RUBY, nil, __FILE__, __LINE__ + 1 38 | def #{field_type}_field(method, **options, &block) 39 | slot = wrap_with_default_slot(method, &block) 40 | Components::SlInput.new(:#{field_class}, object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render(&slot) 41 | end 42 | RUBY 43 | end 44 | 45 | def color_field(method, **options) 46 | Components::SlColorPicker.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render 47 | end 48 | alias color_picker color_field 49 | 50 | def range_field(method, **options) 51 | Components::SlRange.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render 52 | end 53 | alias range range_field 54 | 55 | def switch_field(method, **options, &block) 56 | if block_given? 57 | Components::SlSwitch.new(object_name, method, @template, options.with_defaults(object: @object)).render(&block) 58 | else 59 | Components::SlSwitch.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render(&block) 60 | end 61 | end 62 | alias switch switch_field 63 | 64 | def text_area(method, **options, &block) 65 | Components::SlTextarea.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method), resize: 'auto')).render(&block) 66 | end 67 | 68 | def check_box(method, options = {}, checked_value = "1", unchecked_value = "0", &block) 69 | Components::SlCheckbox.new(object_name, method, @template, checked_value, unchecked_value, options.with_defaults(label: label_text(method)).merge(object: @object)).render(&block) 70 | end 71 | 72 | def select(method, choices = nil, options = {}, html_options = {}, &block) 73 | slot = wrap_with_default_slot(method, &block) 74 | Components::SlSelect.new(object_name, method, @template, choices, options.with_defaults(object: @object), html_options.with_defaults(label: label_text(method)), slot).render 75 | end 76 | 77 | def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}, &block) 78 | slot = wrap_with_default_slot(method, &block) 79 | Components::SlCollectionSelect.new(object_name, method, @template, collection, value_method, text_method, options.with_defaults(object: @object), html_options.with_defaults(label: label_text(method)), slot).render 80 | end 81 | 82 | def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}, &block) 83 | slot = wrap_with_default_slot(method, &block) 84 | Components::SlGroupedCollectionSelect.new(object_name, method, @template, collection, group_method, group_label_method, option_key_method, option_value_method, options.with_defaults(object: @object), html_options.with_defaults(label: label_text(method)), slot).render 85 | end 86 | 87 | def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) 88 | Components::SlCollectionRadioButtons.new(object_name, method, @template, collection, value_method, text_method, options.with_defaults(object: @object, label: label_text(method)), html_options).render(&block) 89 | end 90 | 91 | def submit(value = nil, options = {}) 92 | value, options = nil, value if value.is_a?(Hash) 93 | 94 | @template.sl_submit_tag(value || submit_default_value, **options) 95 | end 96 | 97 | private 98 | 99 | def label_text(method, tag_value = nil) 100 | ::ActionView::Helpers::Tags::Label::LabelBuilder.new(@template, object_name.to_s, method.to_s, object, tag_value).translation 101 | end 102 | 103 | def wrap_with_default_slot(method_name, &user_defined_slot) 104 | if default_input_slot_method && !block_given? 105 | -> { @template.method(default_input_slot_method).call(object, method_name) } 106 | else 107 | user_defined_slot 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /app/helpers/shoelace/sl_form_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './sl_form_builder' 4 | 5 | module Shoelace 6 | module SlFormHelper 7 | ShoelaceFormBuilder = SlFormBuilder 8 | deprecate_constant :ShoelaceFormBuilder 9 | 10 | DEFAULT_FORM_PARAMETERS = { builder: SlFormBuilder } 11 | DIVIDER_TAG = "".html_safe 12 | 13 | private_constant :DEFAULT_FORM_PARAMETERS, :DIVIDER_TAG 14 | 15 | def sl_form_for(*args, **options, &block) 16 | form_for(*args, **DEFAULT_FORM_PARAMETERS, **options, &block) 17 | end 18 | 19 | def sl_form_with(**args, &block) 20 | form_with(**args, **DEFAULT_FORM_PARAMETERS, &block) 21 | end 22 | 23 | # Creates a submit button with the text value as the caption, with the +submit+ attribute. 24 | def sl_submit_tag(value = 'Save changes', **options) 25 | options = options.deep_stringify_keys 26 | tag_options = { "type" => "submit", "variant" => "primary" }.update(options) 27 | set_default_disable_with(value, tag_options) 28 | 29 | content_tag('sl-button', value, tag_options) 30 | end 31 | 32 | # Creates a shoelace text field; use these text fields to input smaller chunks of text like a username or a search 33 | # query. 34 | # 35 | # For the properties available on this tag, please refer to the official documentation: 36 | # https://shoelace.style/components/input?id=properties 37 | # 38 | def sl_text_field_tag(name, value = nil, **options, &block) 39 | content_tag('sl-input', '', { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys), &block) 40 | end 41 | 42 | # Returns a string of ++ tags, like +options_for_select+, but prepends a ++ tag to 43 | # each group. 44 | def grouped_sl_options_for_select(grouped_options, options) 45 | body = "".html_safe 46 | 47 | grouped_options.each_with_index do |container, index| 48 | label, values = container 49 | 50 | body.safe_concat(DIVIDER_TAG) if index > 0 51 | body.safe_concat(content_tag("small", label)) if label.present? 52 | body.safe_concat(sl_options_for_select(values, options)) 53 | end 54 | 55 | body 56 | end 57 | 58 | # Accepts an enumerable (hash, array, enumerable, your type) and returns a string of +sl-option+ tags. Given 59 | # an enumerable where the elements respond to +first+ and +last+ (such as a two-element array), the “lasts” serve 60 | # as option values and the “firsts” as option text. 61 | def sl_options_for_select(enumerable, options = nil) 62 | return enumerable if String === enumerable 63 | 64 | selected, disabled = extract_selected_and_disabled(options).map { |r| Array(r).map(&:to_s) } 65 | 66 | enumerable.map do |element| 67 | html_attributes = option_html_attributes(element) 68 | text, value = option_text_and_value(element).map(&:to_s) 69 | 70 | html_attributes[:checked] ||= selected.include?(value) 71 | html_attributes[:disabled] ||= disabled.include?(value) 72 | html_attributes[:value] = value 73 | 74 | tag_builder.content_tag_string('sl-option', text, html_attributes) 75 | end.join("\n").html_safe 76 | end 77 | 78 | def sl_option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) 79 | body = "".html_safe 80 | 81 | collection.each_with_index do |group, index| 82 | option_tags = sl_options_from_collection_for_select(value_for_collection(group, group_method), option_key_method, option_value_method, selected_key) 83 | 84 | body.safe_concat(DIVIDER_TAG) if index > 0 85 | body.safe_concat(content_tag("small", value_for_collection(group, group_label_method))) 86 | body.safe_concat(option_tags) 87 | end 88 | 89 | body 90 | end 91 | 92 | # Returns a string of ++ tags compiled by iterating over the collection and assigning the result of 93 | # a call to the +value_method+ as the option value and the +text_method+ as the option text. 94 | def sl_options_from_collection_for_select(collection, value_method, text_method, selected = nil) 95 | options = collection.map do |element| 96 | [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)] 97 | end 98 | 99 | selected, disabled = extract_selected_and_disabled(selected) 100 | 101 | select_deselect = { 102 | selected: extract_values_from_collection(collection, value_method, selected), 103 | disabled: extract_values_from_collection(collection, value_method, disabled) 104 | } 105 | 106 | sl_options_for_select(options, select_deselect) 107 | end 108 | 109 | # Returns a ++ tag for accessing a specified attribute (identified by method) on an object assigned to 110 | # the template (identified by object). If the current value of method is +tag_value+ the radio button will be 111 | # checked. 112 | # 113 | # To force the radio button to be checked pass checked: true in the options hash. You may pass HTML options there 114 | # as well. 115 | def sl_radio_button(object_name, method, tag_value, options = {}, &block) 116 | Components::SlRadioButton.new(object_name, method, self, tag_value, options).render(&block) 117 | end 118 | 119 | { 120 | email: :email, 121 | number: :number, 122 | password: :password, 123 | search: :search, 124 | telephone: :tel, 125 | phone: :tel, 126 | url: :url 127 | }.each do |field_type, field_class| 128 | # def sl_email_field_tag(method, **options, &block) 129 | # sl_text_field_tag(name, value, options.merge(type: :email)) 130 | # end 131 | eval <<-RUBY, nil, __FILE__, __LINE__ + 1 132 | # Creates a text field of type “#{field_type}”. 133 | def sl_#{field_type}_field_tag(method, **options, &block) 134 | sl_text_field_tag(name, value, options.merge(type: :#{field_class})) 135 | end 136 | RUBY 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /app/helpers/shoelace/tag_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Shoelace 4 | module TagHelper 5 | # Creates a generic ++ element. 6 | def sl_button_tag(**attrs, &block) 7 | content_tag("sl-button", **attrs, &block) 8 | end 9 | 10 | # Creates an tag with the href value as the caption. 11 | def sl_button_to(body, href = nil, **attrs, &block) 12 | if block_given? 13 | sl_button_tag(href: body, **(href || {}), **attrs, &block) 14 | else 15 | sl_button_tag(href: href, **attrs) { body } 16 | end 17 | end 18 | 19 | def sl_icon_tag(name, **attrs) 20 | tag.sl_icon(name: name, **attrs) 21 | end 22 | 23 | def sl_avatar_tag(source, **attrs, &block) 24 | tag.sl_avatar(image: source, **attrs, &block) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "shoelace/rails" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /gemfiles/rails_50.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails-dom-testing", git: "https://github.com/rails/rails-dom-testing.git", ref: "8f5acdfc" 7 | gem "rails", "~> 5.0.0" 8 | gem "railties", "~> 5.0.0" 9 | gem "activesupport", "~> 5.0.0" 10 | gem "minitest", "5.10.3" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails_51.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails-dom-testing", git: "https://github.com/rails/rails-dom-testing.git", ref: "8f5acdfc" 7 | gem "rails", "~> 5.1.0" 8 | gem "railties", "~> 5.1.0" 9 | gem "activesupport", "~> 5.1.0" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_52.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails-dom-testing", git: "https://github.com/rails/rails-dom-testing.git", ref: "8f5acdfc" 7 | gem "rails", "~> 5.2.0" 8 | gem "railties", "~> 5.2.0" 9 | gem "activesupport", "~> 5.2.0" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_60.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails", "~> 6.0.0" 7 | gem "railties", "~> 6.0.0" 8 | gem "activesupport", "~> 6.0.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_61.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails", "~> 6.1.0" 7 | gem "railties", "~> 6.1.0" 8 | gem "activesupport", "~> 6.1.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_70.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails", "~> 7.0.0" 7 | gem "railties", "~> 7.0.0" 8 | gem "activesupport", "~> 7.0.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_71.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails", "~> 7.1.0" 7 | gem "railties", "~> 7.1.0" 8 | gem "activesupport", "~> 7.1.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_72.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "rails", "~> 7.2.0" 7 | gem "railties", "~> 7.2.0" 8 | gem "activesupport", "~> 7.2.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_edge.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | git "https://github.com/rails/rails.git", branch: "main" do 6 | gem "rails" 7 | gem "railties" 8 | gem "activesupport" 9 | end 10 | 11 | gem "rake", "~> 13.0" 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /lib/shoelace/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Shoelace 4 | class Engine < ::Rails::Engine #:nodoc: 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/shoelace/rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "rails/version" 4 | 5 | if defined?(::Rails::Railtie) 6 | require_relative "engine" 7 | require_relative "railtie" 8 | end 9 | 10 | module Shoelace 11 | module Rails 12 | class Error < StandardError; end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/shoelace/rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Shoelace 4 | module Rails 5 | VERSION = "0.8.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/shoelace/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'action_dispatch/middleware/static' 4 | 5 | module Shoelace 6 | mattr_accessor :invalid_input_class_name 7 | self.invalid_input_class_name = nil 8 | 9 | # The only reason this class exists is to clarify that we have a custom static file server after 10 | # `ActionDispatch::Static`. We could just use `ActionDispatch::Static` directly, but it would make the result of 11 | # `rake middleware` more difficult to understand, as the output would look like: 12 | # 13 | # use ... 14 | # use ActionDispatch::Static 15 | # use ActionDispatch::Static # Why do we use the same middleware twice? 16 | # use ... 17 | # 18 | # It is much more straightforward if it looks like: 19 | # 20 | # use ... 21 | # use ActionDispatch::Static 22 | # use Shoelace::AssetProvider 23 | # use ... 24 | # 25 | class AssetProvider < ActionDispatch::Static; end 26 | 27 | class Railtie < ::Rails::Railtie #:nodoc: 28 | config.shoelace = ActiveSupport::OrderedOptions.new 29 | 30 | # Path to the shoelace assets. 31 | config.shoelace.dist_path = "node_modules/@shoelace-style/shoelace/dist" 32 | 33 | # Deprecated. 34 | config.shoelace.invalid_input_class_name = nil 35 | 36 | initializer "shoelace.use_rack_middleware" do |app| 37 | icon_dir = File.join(app.paths["public"].first, "assets/icons") 38 | 39 | if !Dir.exist?(icon_dir) 40 | path = app.root.join(app.config.shoelace.dist_path).to_s 41 | headers = app.config.public_file_server.headers || {} 42 | 43 | app.config.middleware.insert_after ActionDispatch::Static, Shoelace::AssetProvider, path, index: "index.html", headers: headers 44 | end 45 | end 46 | 47 | initializer "shoelace.form_helper" do |app| 48 | # This exists only for maintaining backwards compatibility with the `invalid_input_class_name` option. 49 | end 50 | 51 | rake_tasks do 52 | load "tasks/shoelace.rake" 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/shoelace/testing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Shoelace 4 | module Testing 5 | def sl_select(option_text, from: ) 6 | select_element = find("sl-select[placeholder=\"#{from}\"]") 7 | select_element.click 8 | 9 | within select_element do 10 | find('sl-option', text: option_text).click 11 | end 12 | 13 | select_element 14 | end 15 | 16 | def sl_multi_select(*options_to_select, from: ) 17 | select_element = find("sl-select[placeholder=\"#{from}\"]") 18 | 19 | select_element.click 20 | within select_element do 21 | options_to_select.each do |option_text| 22 | find('sl-option', text: option_text).click 23 | end 24 | end 25 | 26 | # The multi select does not close automatically, so need to click to close it. 27 | select_element.click if options_to_select.size > 1 28 | 29 | select_element 30 | end 31 | 32 | def sl_check(label) 33 | find("sl-checkbox", text: label).click 34 | end 35 | 36 | def sl_toggle(label) 37 | find("sl-switch", text: label).click 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tasks/shoelace.rake: -------------------------------------------------------------------------------- 1 | namespace :shoelace do 2 | namespace :icons do 3 | desc "Copy Shoelace icons to the assets path" 4 | task copy: :environment do 5 | cp_r Rails.root.join(Shoelace::Railtie.config.shoelace.dist_path, "assets").to_s, Rails.public_path 6 | end 7 | 8 | desc "Remove Shoelace icons" 9 | task clobber: :environment do 10 | rm_rf File.join(Rails.public_path, "assets/icons") 11 | end 12 | end 13 | end 14 | 15 | # Make sure `yarn install` is run before running `shoelace:icons:copy`. 16 | if Rake::Task.task_defined?("javascript:build") 17 | Rake::Task["shoelace:icons:copy"].enhance(["javascript:build"]) 18 | end 19 | 20 | if Rake::Task.task_defined?("assets:precompile") 21 | Rake::Task["assets:precompile"].enhance(["shoelace:icons:copy"]) 22 | else 23 | Rake::Task.define_task('assets:precompile' => ['shoelace:icons:copy']) 24 | end 25 | -------------------------------------------------------------------------------- /shoelace-rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/shoelace/rails/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "shoelace-rails" 7 | spec.version = Shoelace::Rails::VERSION 8 | spec.authors = ["Yuki Nishijima"] 9 | spec.email = ["yuki24@hey.com"] 10 | 11 | spec.summary = "Rails view helpers Shoelace.style, the design system." 12 | spec.description = "The shoelace-rails gem adds useful view helper methods for using Shoalace Web Components." 13 | spec.homepage = "https://github.com/yuki24/shoelace-rails" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 2.5.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = "https://github.com/yuki24/shoelace-rails" 19 | spec.metadata["changelog_uri"] = "https://github.com/yuki24/shoelace-rails/releases" 20 | 21 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 22 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A?:test/}) } 23 | end 24 | 25 | spec.bindir = "exe" 26 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 27 | spec.require_paths = ["lib"] 28 | 29 | spec.add_dependency "actionview", ">= 5.2.0" 30 | spec.add_dependency "actionpack", ">= 5.2.0" 31 | 32 | spec.add_development_dependency "appraisal" 33 | spec.add_development_dependency "minitest", ">= 5.14.4" 34 | spec.add_development_dependency "rails-dom-testing", ">= 2.2.0" 35 | end 36 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_checkbox_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlCheckboxTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#check_box" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.check_box(:name) 12 | 13 | Name 14 | HTML 15 | end 16 | end 17 | 18 | test "#check_box with an invalid value" do 19 | yuki = User.new.tap(&:validate) 20 | 21 | sl_form_for(yuki, url: "/") do |form| 22 | assert_dom_equal <<~HTML, form.check_box(:name) 23 | 24 | Name 25 | HTML 26 | end 27 | end 28 | 29 | test "#check_box with a block" do 30 | sl_form_for(User.new, url: "/") do |form| 31 | assert_dom_equal <<~HTML, form.check_box(:name) { "Maintainer Name" } 32 | 33 | Maintainer Name 34 | HTML 35 | end 36 | end 37 | 38 | test "#check_box without a hidden input" do 39 | sl_form_for(User.new, url: "/") do |form| 40 | assert_dom_equal <<~HTML, form.check_box(:name, include_hidden: false) { "Maintainer Name" } 41 | Maintainer Name 42 | HTML 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_color_picker_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlColorPickerTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#color_field" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.color_field(:name) 12 | 13 | HTML 14 | end 15 | end 16 | 17 | test "#color_field with an invalid value" do 18 | yuki = User.new.tap(&:validate) 19 | 20 | sl_form_for(yuki, url: "/") do |form| 21 | assert_dom_equal <<~HTML, form.color_field(:name) 22 | 23 | HTML 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_input_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlInputTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#text_field" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.text_field(:name) 12 | 13 | HTML 14 | end 15 | end 16 | 17 | test "#text_field with a default value" do 18 | sl_form_for(User.new(name: "Yuki"), url: "/") do |form| 19 | assert_dom_equal <<~HTML, form.text_field(:name) 20 | 21 | HTML 22 | end 23 | end 24 | 25 | test "#text_field with an invalid value" do 26 | yuki = User.new.tap(&:validate) 27 | 28 | sl_form_for(yuki, url: "/") do |form| 29 | assert_dom_equal <<~HTML, form.text_field(:name) 30 | 31 | HTML 32 | end 33 | end 34 | 35 | test "#date_field" do 36 | sl_form_for(User.new, url: "/") do |form| 37 | assert_dom_equal <<~HTML, form.date_field(:name) 38 | 39 | HTML 40 | end 41 | end 42 | 43 | test "#email_field" do 44 | sl_form_for(User.new, url: "/") do |form| 45 | assert_dom_equal <<~HTML, form.email_field(:name) 46 | 47 | HTML 48 | end 49 | end 50 | 51 | test "#number_field" do 52 | sl_form_for(User.new, url: "/") do |form| 53 | assert_dom_equal <<~HTML, form.number_field(:name) 54 | 55 | HTML 56 | end 57 | end 58 | 59 | test "#password_field" do 60 | sl_form_for(User.new, url: "/") do |form| 61 | assert_dom_equal <<~HTML, form.password_field(:name) 62 | 63 | HTML 64 | end 65 | end 66 | 67 | test "#search_field" do 68 | sl_form_for(User.new, url: "/") do |form| 69 | assert_dom_equal <<~HTML, form.search_field(:name) 70 | 71 | HTML 72 | end 73 | end 74 | 75 | test "#telephone_field" do 76 | sl_form_for(User.new, url: "/") do |form| 77 | assert_dom_equal <<~HTML, form.telephone_field(:name) 78 | 79 | HTML 80 | end 81 | end 82 | 83 | test "#phone_field" do 84 | sl_form_for(User.new, url: "/") do |form| 85 | assert_dom_equal <<~HTML, form.phone_field(:name) 86 | 87 | HTML 88 | end 89 | end 90 | 91 | test "#text_field with a block" do 92 | sl_form_for(User.new(name: "Yuki"), url: "/") do |form| 93 | assert_dom_equal <<~HTML, form.text_field(:name) { 'slot' } 94 | slot 95 | HTML 96 | end 97 | end 98 | 99 | test "#text_field with a default help text block" do 100 | with_default_input_slot_method do 101 | sl_form_for(User.new(name: "Yuki"), url: "/") do |form| 102 | assert_dom_equal <<~HTML, form.text_field(:name) 103 | 104 |
Help text for name Yuki
105 |
106 | HTML 107 | end 108 | 109 | sl_form_for(User.new(name: "Yuki"), url: "/") do |form| 110 | assert_dom_equal <<~HTML, form.text_field(:name) { "slot" } 111 | 112 | slot 113 | 114 | HTML 115 | end 116 | end 117 | end 118 | 119 | test "#url_field" do 120 | sl_form_for(User.new, url: "/") do |form| 121 | assert_dom_equal <<~HTML, form.url_field(:name) 122 | 123 | HTML 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_radio_group_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlRadioGroupTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#collection_radio_buttons" do 10 | users = { 11 | 1 => "Yuki Nishijima", 12 | 2 => "Matz", 13 | 3 => "Koichi Sasada", 14 | } 15 | 16 | sl_form_for(User.new, url: "/") do |form| 17 | assert_dom_equal <<~HTML, form.collection_radio_buttons(:name, users, :first, :last) 18 | 19 | Yuki Nishijima 20 | Matz 21 | Koichi Sasada 22 | 23 | HTML 24 | end 25 | end 26 | 27 | test "#collection_radio_buttons with an invalid value" do 28 | yuki = User.new.tap(&:validate) 29 | users = { 30 | 1 => "Yuki Nishijima", 31 | 2 => "Matz", 32 | 3 => "Koichi Sasada", 33 | } 34 | 35 | sl_form_for(yuki, url: "/") do |form| 36 | assert_dom_equal <<~HTML, form.collection_radio_buttons(:name, users, :first, :last) 37 | 38 | Yuki Nishijima 39 | Matz 40 | Koichi Sasada 41 | 42 | HTML 43 | end 44 | end 45 | 46 | test "#collection_radio_buttons with a default value" do 47 | users = { 48 | 1 => "Yuki Nishijima", 49 | 2 => "Matz", 50 | 3 => "Koichi Sasada", 51 | } 52 | 53 | sl_form_for(User.new(name: 1), url: "/") do |form| 54 | assert_dom_equal <<~HTML, form.collection_radio_buttons(:name, users, :first, :last) 55 | 56 | Yuki Nishijima 57 | Matz 58 | Koichi Sasada 59 | 60 | HTML 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_range_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlRangeTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#range_field" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.range_field(:name) 12 | 13 | HTML 14 | end 15 | end 16 | 17 | test "#range_field with an invalid value" do 18 | yuki = User.new.tap(&:validate) 19 | 20 | sl_form_for(yuki, url: "/") do |form| 21 | assert_dom_equal <<~HTML, form.range_field(:name) 22 | 23 | HTML 24 | end 25 | end 26 | 27 | test "#range_field without a label" do 28 | sl_form_for(User.new, url: "/") do |form| 29 | assert_dom_equal <<~HTML, form.range_field(:name, label: nil) 30 | 31 | HTML 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_select_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlSelectTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#select" do 10 | users = { 11 | "Yuki Nishijima" => 1, 12 | "Matz" => 2, 13 | "Koichi Sasada" => 3 14 | } 15 | 16 | sl_form_for(User.new, url: "/") do |form| 17 | assert_dom_equal <<~HTML, form.select(:name, users) 18 | 19 | Yuki Nishijima 20 | Matz 21 | Koichi Sasada 22 | 23 | HTML 24 | end 25 | end 26 | 27 | test "#select with an invalid value" do 28 | yuki = User.new.tap(&:validate) 29 | users = { 30 | "Yuki Nishijima" => 1, 31 | "Matz" => 2, 32 | "Koichi Sasada" => 3 33 | } 34 | 35 | sl_form_for(yuki, url: "/") do |form| 36 | assert_dom_equal <<~HTML, form.select(:name, users) 37 | 38 | Yuki Nishijima 39 | Matz 40 | Koichi Sasada 41 | 42 | HTML 43 | end 44 | end 45 | 46 | test "#select with a default help text block" do 47 | with_default_input_slot_method do 48 | sl_form_for(User.new, url: "/") do |form| 49 | assert_dom_equal <<~HTML, form.select(:name, "Yuki Nishijima" => 1) 50 | 51 | Yuki Nishijima 52 |
Help text for name
53 |
54 | HTML 55 | end 56 | end 57 | end 58 | 59 | test "#select with a block that does not return anything" do 60 | sl_form_for(User.new, url: "/") do |form| 61 | assert_dom_equal <<~HTML, form.select(:name, "Yuki Nishijima" => 1) { } 62 | 63 | Yuki Nishijima 64 | 65 | HTML 66 | end 67 | end 68 | 69 | test "#select with a custom value" do 70 | users = { 71 | "Yuki Nishijima" => 1, 72 | "Matz" => 2, 73 | "Koichi Sasada" => 3 74 | } 75 | 76 | sl_form_for(User.new, url: "/") do |form| 77 | assert_dom_equal <<~HTML, form.select(:name, users, {}, { value: 3 }) 78 | 79 | Yuki Nishijima 80 | Matz 81 | Koichi Sasada 82 | 83 | HTML 84 | end 85 | end 86 | 87 | test "#select with custom selected and disabled values" do 88 | users = { 89 | "Yuki Nishijima" => 1, 90 | "Matz" => 2, 91 | "Koichi Sasada" => 3 92 | } 93 | 94 | sl_form_for(User.new, url: "/") do |form| 95 | assert_dom_equal <<~HTML, form.select(:name, users, selected: 3, disabled: 1) 96 | 97 | Yuki Nishijima 98 | Matz 99 | Koichi Sasada 100 | 101 | HTML 102 | end 103 | end 104 | 105 | test "#select with multiple" do 106 | users = { 107 | "Yuki Nishijima" => 1, 108 | "Matz" => 2, 109 | "Koichi Sasada" => 3 110 | } 111 | 112 | sl_form_for(User.new, url: "/") do |form| 113 | assert_dom_equal <<~HTML, form.select(:name, users, {}, { multiple: true }) 114 | 115 | Yuki Nishijima 116 | Matz 117 | Koichi Sasada 118 | 119 | HTML 120 | end 121 | end 122 | 123 | test "#select with grouped options" do 124 | users = { 125 | "Main maintainers" => [ 126 | ["Matz", 2], 127 | ["Koichi Sasada", 3] 128 | ], 129 | "Default gem maintainers" => [ 130 | ["Yuki Nishijima", 1], 131 | ] 132 | } 133 | 134 | sl_form_for(User.new, url: "/") do |form| 135 | assert_dom_equal <<~HTML, form.select(:name, users) 136 | 137 | Main maintainers 138 | Matz 139 | Koichi Sasada 140 | 141 | Default gem maintainers 142 | Yuki Nishijima 143 | 144 | HTML 145 | end 146 | end 147 | 148 | test "#select with grouped options with a default value" do 149 | users = { 150 | "Main maintainers" => [ 151 | ["Matz", 2], 152 | ["Koichi Sasada", 3] 153 | ], 154 | "Default gem maintainers" => [ 155 | ["Yuki Nishijima", 1], 156 | ] 157 | } 158 | 159 | sl_form_for(User.new(name: 2), url: "/") do |form| 160 | assert_dom_equal <<~HTML, form.select(:name, users) 161 | 162 | Main maintainers 163 | Matz 164 | Koichi Sasada 165 | 166 | Default gem maintainers 167 | Yuki Nishijima 168 | 169 | HTML 170 | end 171 | end 172 | 173 | test "#collection_select" do 174 | users = { 175 | 1 => "Yuki Nishijima", 176 | 2 => "Matz", 177 | 3 => "Koichi Sasada", 178 | } 179 | 180 | sl_form_for(User.new, url: "/") do |form| 181 | assert_dom_equal <<~HTML, form.collection_select(:name, users, :first, :last) 182 | 183 | Yuki Nishijima 184 | Matz 185 | Koichi Sasada 186 | 187 | HTML 188 | end 189 | end 190 | 191 | test "#collection_select with a default help text block" do 192 | with_default_input_slot_method do 193 | sl_form_for(User.new, url: "/") do |form| 194 | assert_dom_equal <<~HTML, form.collection_select(:name, { 1 => "Yuki Nishijima" }, :first, :last) 195 | 196 | Yuki Nishijima 197 |
Help text for name
198 |
199 | HTML 200 | end 201 | end 202 | end 203 | 204 | test "#collection_select with an invalid value" do 205 | yuki = User.new.tap(&:validate) 206 | users = { 207 | 1 => "Yuki Nishijima", 208 | 2 => "Matz", 209 | 3 => "Koichi Sasada", 210 | } 211 | 212 | sl_form_for(yuki, url: "/") do |form| 213 | assert_dom_equal <<~HTML, form.collection_select(:name, users, :first, :last) 214 | 215 | Yuki Nishijima 216 | Matz 217 | Koichi Sasada 218 | 219 | HTML 220 | end 221 | end 222 | 223 | test "#collection_select with a default value" do 224 | users = { 225 | 1 => "Yuki Nishijima", 226 | 2 => "Matz", 227 | 3 => "Koichi Sasada", 228 | } 229 | 230 | sl_form_for(User.new(name: "2"), url: "/") do |form| 231 | assert_dom_equal <<~HTML, form.collection_select(:name, users, :first, :last) 232 | 233 | Yuki Nishijima 234 | Matz 235 | Koichi Sasada 236 | 237 | HTML 238 | end 239 | end 240 | 241 | test "#grouped_collection_select" do 242 | users = [ 243 | OpenStruct.new( 244 | group_name: "Main maintainers", 245 | members: [ 246 | OpenStruct.new(id: 1, name: "Matz"), 247 | OpenStruct.new(id: 2, name: "Koichi Sasada"), 248 | ] 249 | ), 250 | OpenStruct.new( 251 | group_name: "Default gem maintainers", 252 | members: [OpenStruct.new(id: 3, name: "Yuki Nishijima")] 253 | ), 254 | ] 255 | 256 | sl_form_for(User.new(name: "2"), url: "/") do |form| 257 | assert_dom_equal <<~HTML, form.grouped_collection_select(:name, users, :members, :group_name, :id, :name) 258 | 259 | Main maintainers 260 | Matz 261 | Koichi Sasada 262 | 263 | Default gem maintainers 264 | Yuki Nishijima 265 | 266 | HTML 267 | end 268 | end 269 | 270 | test "#grouped_collection_select with a default help text block" do 271 | users = [ 272 | OpenStruct.new( 273 | group_name: "Default gem maintainers", 274 | members: [OpenStruct.new(id: 3, name: "Yuki Nishijima")] 275 | ), 276 | ] 277 | 278 | with_default_input_slot_method do 279 | sl_form_for(User.new(name: "2"), url: "/") do |form| 280 | assert_dom_equal <<~HTML, form.grouped_collection_select(:name, users, :members, :group_name, :id, :name) 281 | 282 | Default gem maintainers 283 | Yuki Nishijima 284 |
Help text for name 2
285 |
286 | HTML 287 | end 288 | end 289 | end 290 | 291 | test "#grouped_collection_select with an invalid value" do 292 | yuki = User.new.tap(&:validate) 293 | users = [ 294 | OpenStruct.new( 295 | group_name: "Main maintainers", 296 | members: [ 297 | OpenStruct.new(id: 1, name: "Matz"), 298 | OpenStruct.new(id: 2, name: "Koichi Sasada"), 299 | ] 300 | ), 301 | OpenStruct.new( 302 | group_name: "Default gem maintainers", 303 | members: [OpenStruct.new(id: 3, name: "Yuki Nishijima")] 304 | ), 305 | ] 306 | 307 | sl_form_for(yuki, url: "/") do |form| 308 | assert_dom_equal <<~HTML, form.grouped_collection_select(:name, users, :members, :group_name, :id, :name) 309 | 310 | Main maintainers 311 | Matz 312 | Koichi Sasada 313 | 314 | Default gem maintainers 315 | Yuki Nishijima 316 | 317 | HTML 318 | end 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_switch_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlRangeTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#switch_field" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.switch_field(:name) 12 | Name 13 | HTML 14 | end 15 | end 16 | 17 | test "#switch_field with an invalid value" do 18 | yuki = User.new.tap(&:validate) 19 | 20 | sl_form_for(yuki, url: "/") do |form| 21 | assert_dom_equal <<~HTML, form.switch_field(:name) 22 | Name 23 | HTML 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/helpers/form_builder/sl_textarea_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../../app/helpers/shoelace/sl_form_helper' 5 | 6 | class FormBuilderSlTextareaTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#text_area" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.text_area(:name) 12 | 13 | HTML 14 | end 15 | end 16 | 17 | test "#text_area with an invalid value" do 18 | yuki = User.new.tap(&:validate) 19 | 20 | sl_form_for(yuki, url: "/") do |form| 21 | assert_dom_equal <<~HTML, form.text_area(:name) 22 | 23 | HTML 24 | end 25 | end 26 | 27 | test "#text_area with a value" do 28 | sl_form_for(User.new(name: "Yuki Nishijima"), url: "/") do |form| 29 | assert_dom_equal <<~HTML, form.text_area(:name) 30 | 31 | HTML 32 | end 33 | end 34 | 35 | test "#text_area with an one-off value" do 36 | sl_form_for(User.new(name: "Yuki Nishijima"), url: "/") do |form| 37 | assert_dom_equal <<~HTML, form.text_area(:name, value: "Yuki") 38 | 39 | HTML 40 | end 41 | end 42 | 43 | test "#text_area without a label" do 44 | sl_form_for(User.new, url: "/") do |form| 45 | assert_dom_equal <<~HTML, form.text_area(:name, label: nil) 46 | 47 | HTML 48 | end 49 | end 50 | 51 | test "#text_area with a size" do 52 | sl_form_for(User.new, url: "/") do |form| 53 | assert_dom_equal <<~HTML, form.text_area(:name, size: "small") 54 | 55 | HTML 56 | end 57 | end 58 | 59 | test "#text_area with a block" do 60 | sl_form_for(User.new, url: "/") do |form| 61 | expected = <<~HTML 62 | 63 |
Name can not be blank.
64 |
65 | HTML 66 | 67 | assert_dom_equal(expected, form.text_area(:name) { 68 | content_tag(:div, "Name can not be blank.", slot: "help-text") 69 | }) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/helpers/form_builder_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../app/helpers/shoelace/sl_form_builder' 5 | 6 | class FormBuilderTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#submit" do 10 | sl_form_for(User.new, url: "/") do |form| 11 | assert_dom_equal <<~HTML, form.submit("Save") 12 | Save 13 | HTML 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/helpers/form_helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../app/helpers/shoelace/sl_form_builder' 5 | 6 | class SlFormHelperTest < ActionView::TestCase 7 | include Shoelace::SlFormHelper 8 | 9 | test "#sl_text_field_tag with name and value" do 10 | assert_dom_equal <<~HTML, sl_text_field_tag('name', 'Your name') 11 | 12 | HTML 13 | end 14 | 15 | test "#sl_text_field_tag with class string" do 16 | assert_dom_equal <<~HTML, sl_text_field_tag('name', 'Your name', class: "admin") 17 | 18 | HTML 19 | end 20 | 21 | test "#sl_text_field_tag with params" do 22 | assert_raises ActionController::UnfilteredParameters do 23 | sl_text_field_tag('name', 'Your name', **ActionController::Parameters.new(key: "value")) 24 | end 25 | end 26 | 27 | test "#sl_text_field_tag with disabled: true" do 28 | assert_dom_equal <<~HTML, sl_text_field_tag('name', 'Your name', disabled: true) 29 | 30 | HTML 31 | end 32 | 33 | test "#sl_submit_tag" do 34 | assert_dom_equal <<~HTML, sl_submit_tag("Save") 35 | Save 36 | HTML 37 | end 38 | 39 | test "#sl_submit_tag with onclick" do 40 | assert_dom_equal <<~HTML, sl_submit_tag("Save", onclick: "alert('hello!')", data: { disable_with: "Saving..." }) 41 | Save 42 | HTML 43 | end 44 | 45 | test "#sl_radio_button" do 46 | assert_dom_equal(<<~HTML, sl_radio_button(:user, :name, 'userid-314', checked: true) { "Yuki Nishijima" }) 47 | Yuki Nishijima 48 | HTML 49 | end 50 | 51 | test "#sl_form_with" do 52 | assert_dom_equal(<<~HTML, sl_form_with(url: "/") {}) 53 |
54 | 55 | 56 | HTML 57 | end 58 | 59 | test "#sl_form_for" do 60 | assert_dom_equal(<<~HTML, sl_form_for(User.new, url: "/") { }) 61 | 62 | 63 | 64 | HTML 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/helpers/tag_helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../app/helpers/shoelace/tag_helper' 5 | 6 | class TagHelperTest < ActionView::TestCase 7 | include Shoelace::TagHelper 8 | 9 | test "#sl_button_tag" do 10 | assert_dom_equal(<<~HTML, sl_button_tag { "Submit" }) 11 | Submit 12 | HTML 13 | end 14 | 15 | test "#sl_button_to" do 16 | assert_dom_equal <<~HTML, sl_button_to("Next", "/next") 17 | Next 18 | HTML 19 | 20 | assert_dom_equal <<~HTML, sl_button_to("Next", "/next", class: "mt-1") 21 | Next 22 | HTML 23 | 24 | assert_dom_equal(<<~HTML, sl_button_to("/next") { "Next" }) 25 | Next 26 | HTML 27 | 28 | assert_dom_equal(<<~HTML, sl_button_to("/next", class: "mt-1") { "Next" }) 29 | Next 30 | HTML 31 | 32 | assert_dom_equal <<~HTML, sl_button_to("Next") 33 | Next 34 | HTML 35 | end 36 | 37 | test "#sl_icon_tag"do 38 | assert_dom_equal <<~HTML, sl_icon_tag("0-circle-fill") 39 | 40 | HTML 41 | 42 | assert_dom_equal <<~HTML, sl_icon_tag("0-circle-fill", slot: "icon") 43 | 44 | HTML 45 | end 46 | 47 | test "#sl_avatar_tag"do 48 | assert_dom_equal <<~HTML, sl_avatar_tag("/path/to/image.jpg") 49 | 50 | HTML 51 | 52 | assert_dom_equal(<<~HTML, sl_avatar_tag("/path/to/image.jpg", slot: "trigger") { "Body" }) 53 | 54 | Body 55 | 56 | HTML 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/helpers/translation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test_helper' 3 | 4 | require_relative '../../app/helpers/shoelace/sl_form_builder' 5 | 6 | class TranslationTest < ActionView::TestCase 7 | include ActionView::Helpers::TranslationHelper 8 | include Shoelace::SlFormHelper 9 | 10 | setup do 11 | I18n.backend.store_translations :en, 12 | helpers: { 13 | label: { 14 | user: { name: "Full Name" } 15 | } 16 | } 17 | 18 | view_paths = ActionController::Base.view_paths 19 | view_paths.each(&:clear_cache) 20 | @view = ::ActionView::Base.with_empty_template_cache.with_view_paths(view_paths, {}) 21 | end 22 | 23 | teardown do 24 | I18n.backend.reload! 25 | end 26 | 27 | test "Form helpers should respect label translations" do 28 | sl_form_for(User.new, url: "/") do |form| 29 | assert_dom_equal <<~HTML, form.text_field(:name) 30 | 31 | HTML 32 | end 33 | end 34 | 35 | test "Form helpers should cast symbol object names to String" do 36 | sl_form_for(User.new, as: :user, url: "/") do |form| 37 | assert_dom_equal <<~HTML, form.text_field(:name) 38 | 39 | HTML 40 | end 41 | end 42 | 43 | test "Form helpers should fall back to the humanize method when there is no matching translation" do 44 | I18n.backend.reload! 45 | 46 | sl_form_for(OpenStruct.new, as: :user, url: "/") do |form| 47 | assert_dom_equal <<~HTML, form.text_field(:name_eq) 48 | 49 | HTML 50 | end 51 | end 52 | 53 | test "#color_field should respect label translations" do 54 | sl_form_for(User.new, url: "/") do |form| 55 | assert_dom_equal <<~HTML, form.color_field(:name) 56 | 57 | HTML 58 | end 59 | end 60 | 61 | test "#range_field should respect label translations" do 62 | sl_form_for(User.new, url: "/") do |form| 63 | assert_dom_equal <<~HTML, form.range_field(:name) 64 | 65 | HTML 66 | end 67 | end 68 | 69 | test "#switch_field should respect label translations" do 70 | sl_form_for(User.new, url: "/") do |form| 71 | assert_dom_equal <<~HTML, form.switch_field(:name) 72 | Full Name 73 | HTML 74 | end 75 | end 76 | 77 | test "#text_area should respect label translations" do 78 | sl_form_for(User.new, url: "/") do |form| 79 | assert_dom_equal <<~HTML, form.text_area(:name) 80 | 81 | HTML 82 | end 83 | end 84 | 85 | test "#check_box should respect label translations" do 86 | sl_form_for(User.new, url: "/") do |form| 87 | assert_dom_equal <<~HTML, form.check_box(:name) 88 | 89 | Full Name 90 | HTML 91 | end 92 | end 93 | 94 | test "#selec should respect label translationst" do 95 | users = { 96 | "Yuki Nishijima" => 1, 97 | "Matz" => 2, 98 | "Koichi Sasada" => 3 99 | } 100 | 101 | sl_form_for(User.new, url: "/") do |form| 102 | assert_dom_equal <<~HTML, form.select(:name, users) 103 | 104 | Yuki Nishijima 105 | Matz 106 | Koichi Sasada 107 | 108 | HTML 109 | end 110 | end 111 | 112 | test "#collection_select should respect label translations" do 113 | users = { 114 | 1 => "Yuki Nishijima", 115 | 2 => "Matz", 116 | 3 => "Koichi Sasada", 117 | } 118 | 119 | sl_form_for(User.new, url: "/") do |form| 120 | assert_dom_equal <<~HTML, form.collection_select(:name, users, :first, :last) 121 | 122 | Yuki Nishijima 123 | Matz 124 | Koichi Sasada 125 | 126 | HTML 127 | end 128 | end 129 | 130 | test "#grouped_collection_select should respect label translations" do 131 | users = [ 132 | OpenStruct.new( 133 | group_name: "Main maintainers", 134 | members: [ 135 | OpenStruct.new(id: 1, name: "Matz"), 136 | OpenStruct.new(id: 2, name: "Koichi Sasada"), 137 | ] 138 | ), 139 | OpenStruct.new( 140 | group_name: "Default gem maintainers", 141 | members: [OpenStruct.new(id: 3, name: "Yuki Nishijima")] 142 | ), 143 | ] 144 | 145 | sl_form_for(User.new(name: "2"), url: "/") do |form| 146 | assert_dom_equal <<~HTML, form.grouped_collection_select(:name, users, :members, :group_name, :id, :name) 147 | 148 | Main maintainers 149 | Matz 150 | Koichi Sasada 151 | 152 | Default gem maintainers 153 | Yuki Nishijima 154 | 155 | HTML 156 | end 157 | end 158 | 159 | test "#collection_radio_buttons should respect label translations" do 160 | users = { 161 | 1 => "Yuki Nishijima", 162 | 2 => "Matz", 163 | 3 => "Koichi Sasada", 164 | } 165 | 166 | sl_form_for(User.new, url: "/") do |form| 167 | assert_dom_equal <<~HTML, form.collection_radio_buttons(:name, users, :first, :last) 168 | 169 | Yuki Nishijima 170 | Matz 171 | Koichi Sasada 172 | 173 | HTML 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "shoelace/rails" 5 | 6 | require "minitest/autorun" 7 | require "action_controller" 8 | require "action_view" 9 | require "action_view/testing/resolvers" 10 | require "active_model" 11 | 12 | require_relative '../app/helpers/shoelace/sl_form_builder' 13 | 14 | ActionView::TestCase.include(Rails::Dom::Testing::Assertions) 15 | Shoelace::SlFormBuilder.field_error_proc = nil 16 | 17 | class User 18 | include ActiveModel::Model 19 | 20 | attr_accessor :name 21 | 22 | validates :name, presence: true 23 | end 24 | 25 | class ActionView::TestCase 26 | AUTOCOMPLETE_ATTRIBUTE = ActionView::VERSION::STRING >= '6.1.0' ? 'autocomplete="off"' : '' 27 | 28 | def with_default_input_slot_method(input_slot_method = :render_default_slot) 29 | Shoelace::SlFormBuilder.default_input_slot_method = input_slot_method 30 | yield 31 | ensure 32 | Shoelace::SlFormBuilder.default_input_slot_method = nil 33 | end 34 | 35 | private 36 | 37 | def render_default_slot(resource, _attribute) 38 | content_tag(:div, "Help text for #{_attribute} #{resource.name}", slot: "help-text") 39 | end 40 | end 41 | --------------------------------------------------------------------------------