├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app └── helpers │ ├── .keep │ └── attributes_and_token_lists │ └── application_helper.rb ├── attributes_and_token_lists.gemspec ├── bin └── rails ├── config └── routes.rb ├── lib ├── action_view │ ├── attributes.rb │ └── helpers │ │ └── tag_helper │ │ └── tag_builder.rb ├── attributes_and_token_lists.rb ├── attributes_and_token_lists │ ├── engine.rb │ ├── object_backports.rb │ ├── tag_builder.rb │ └── version.rb └── tasks │ └── attributes_and_token_lists_tasks.rake └── test ├── attributes_and_token_lists └── tag_builder_test.rb ├── attributes_and_token_lists_test.rb ├── dummy ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── examples_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── javascript │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ └── concerns │ │ │ └── .keep │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── bin │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── attributes_and_token_lists.html.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── permissions_policy.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── storage.yml ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico ├── helpers └── attributes_and_token_lists │ └── application_helper_test.rb ├── integration ├── helpers_test.rb └── tag_builder_test.rb └── test_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI Tests" 2 | 3 | on: 4 | - "pull_request" 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby-version: 12 | - "2.7" 13 | - "3.0" 14 | - "3.1" 15 | - "3.2" 16 | rails-version: 17 | - "6.1" 18 | - "7.0" 19 | - "main" 20 | 21 | env: 22 | RAILS_VERSION: ${{ matrix.rails-version }} 23 | 24 | name: ${{ format('Tests (Ruby {0}, Rails {1})', matrix.ruby-version, matrix.rails-version) }} 25 | runs-on: "ubuntu-latest" 26 | 27 | steps: 28 | - uses: "actions/checkout@v2" 29 | - uses: "actions/setup-node@v2" 30 | with: 31 | node-version: "14" 32 | - uses: "ruby/setup-ruby@v1" 33 | with: 34 | rubygems: 3.3.13 35 | ruby-version: ${{ matrix.ruby-version }} 36 | bundler-cache: true 37 | 38 | - run: bin/rails test test/**/*_test.rb 39 | - run: bin/rails standard 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /test/dummy/db/*.sqlite3 7 | /test/dummy/db/*.sqlite3-* 8 | /test/dummy/log/*.log 9 | /test/dummy/storage/ 10 | /test/dummy/tmp/ 11 | .byebug_history 12 | 13 | Gemfile.lock 14 | .ruby-version 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The noteworthy changes for each AttributesAndTokenLists version are 4 | included here. For a complete changelog, see the [commits] for each version via 5 | the version links. 6 | 7 | [commits]: https://github.com/seanpdoyle/attributes_and_token_lists 8 | 9 | ## main 10 | 11 | * Drop support for loading `AttributesAndTokenLists.builder` calls from 12 | `app/views/initializers`. 13 | 14 | *Sean Doyle* 15 | 16 | * Replace `AttributesAndTokenLists::Attributes` with `ActionView::Attributes` to 17 | make it more `Hash`-like. 18 | 19 | Replace `AttributesAndTokenLists::TagBuilder#[]` with `#with` so that it can 20 | be more `Hash`-like 21 | 22 | Remove `AttributesAndTokenLists::TokenList` and 23 | `AttributesAndTokenLists::AttributeMerger` and rely on 24 | `ActionView::Helpers::TagHelper#token_list` and `with_options` instead. 25 | 26 | *Sean Doyle* 27 | 28 | ## 0.2.1 (Jan 22, 2023) 29 | 30 | * REVERT: Inline `ActiveSupport::OptionMerger` into `AttributesAndTokenLists::AttributeMerger` to handle `AttributesAndTokenLists::Attributes` instances 31 | 32 | * REVERT: Add `#with_attributes` support to Action View's collection helpers (like [collection_check_boxes][]) in the same style as `FormBuilder#with_attributes` 33 | 34 | ## 0.2.0 (Jan 21, 2023) 35 | 36 | * Add `#with_attributes` support to Action View's collection helpers (like [collection_check_boxes][]) in the same style as `FormBuilder#with_attributes` 37 | 38 | ```ruby 39 | collection_check_boxes :record, :choice, ["a", "b"], :to_s, :to_s do |builder| 40 | builder.with_attributes class: "font-bold" do |styled| 41 | styled.check_box 42 | #=> 43 | end 44 | end 45 | ``` 46 | 47 | [collection_check_boxes]: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-collection_check_boxes 48 | 49 | * Inline `ActiveSupport::OptionMerger` into `AttributesAndTokenLists::AttributeMerger` to handle `AttributesAndTokenLists::Attributes` instances 50 | 51 | * Change: combine variants with `#[]` 52 | 53 | ```ruby 54 | define :button, tag_name: "button", class: "cursor-pointer" do 55 | variant :primary, class: "text-white bg-green-500" 56 | variant :rounded, class: "rounded-full" 57 | end 58 | 59 | # before 60 | ui.button(:primary, :rounded).tag "A button" 61 | 62 | # after 63 | ui.button[:primary, :rounded].tag "A button" 64 | ``` 65 | 66 | ## 0.1.0 (Jan 21, 2023) 67 | 68 | * Bug: support attribute overrides for unclosed elements (for example, 69 | ``) 70 | 71 | * Bug: gracefully resolve `nil` value when combining variants 72 | 73 | * Rename builder domain-specific language to combine `builder`, `base`, and 74 | `variant`: 75 | 76 | ```ruby 77 | ActiveSupport.on_load :attributes_and_token_lists do 78 | builder :ui do 79 | base :button, tag_name: "button", class: "cursor-pointer" do 80 | variant :primary, class: "text-white bg-green-500" 81 | end 82 | end 83 | end 84 | 85 | # Elsewhere 86 | ui.button.primary.tag "A button" 87 | # => 88 | ``` 89 | 90 | 91 | * Alias `define` to `variant`, and add support for combining variants 92 | 93 | ```ruby 94 | ActiveSupport.on_load :attributes_and_token_lists do 95 | define :ui do 96 | define :button, tag_name: "button", class: "cursor-pointer" do 97 | variant :primary, class: "text-white bg-green-500" 98 | variant :rounded, class: "rounded-full" 99 | end 100 | end 101 | end 102 | 103 | # Elsewhere 104 | ui.button(:primary, :rounded).tag "A button" 105 | # => 106 | ``` 107 | 108 | * Move configuration out of `app/views/initializers` and into a more 109 | conventional `config/initializers` file. 110 | 111 | * Pre-define clumps of attributes by calling `AttributesAndTokenLists.define` in 112 | a `config/initializers` file, or an 113 | `app/views/initializers/attributes_and_token_lists.html.erb` template 114 | 115 | ```erb 116 | <%# app/views/initializers/attributes_and_token_lists.html.erb %> 117 | <% 118 | AttributesAndTokenLists.define :atl do 119 | define :button, tag_name: :button, class: "text-white p-4 focus:outline-none focus:ring" do 120 | define :primary, class: "bg-green-500" 121 | define :secondary, class: "bg-blue-500" 122 | define :tertiary, class: "bg-black" 123 | end 124 | end 125 | %> 126 | 127 | <%# elsewhere %> 128 | 129 | <%= atl.tag.button "Unstyled" %> <%# => %> 130 | <%= atl.button.tag "Base" %> <%# => %> 131 | <%= atl.button.primary.tag "Primary" %> <%# => %> 132 | <%= atl.button.secondary.tag "Secondary" %> <%# => %> 133 | <%= atl.button.tertiary.tag "Tertiary" %> <%# => %> 134 | 135 | <%= atl.button.primary.tag.a "Primary", href: "#" %> <%# => Primary %> 136 | <%= atl.button.primary.link_to "Primary", "#" %> <%# => Primary %> 137 | ``` 138 | 139 | * Remove support for `Attributes#+` and `Attributes#|` aliases for 140 | `Attributes#merge` 141 | 142 | * Resolve `Attributes#merge` primitive value override bug 143 | 144 | * Decorate public interfaces instead of monkey-patching private ones 145 | 146 | * Introduce [standard](https://github.com/testdouble/standard) for style 147 | violation linting 148 | 149 | * Enable chaining `#with_attributes` and `#tag` off of `Attributes` instances 150 | and instances of `AttributeMerger` returned by other `#with_attributes` calls 151 | 152 | * Ensure that `Attributes` are compliant with Action View-provided `tag` helpers 153 | 154 | * Add `Attributes#with_attributes` and `Attributes#with_options` alias to enable 155 | decorating and chaining 156 | 157 | * Support chaining view helpers off `Attributes` instances 158 | 159 | ```ruby 160 | styled = tag.attributes(class: "my-link-class") 161 | 162 | styled.link_to("A link", "/") 163 | ``` 164 | 165 | * Deep symbolize `Attributes` keys when splatting or calling 166 | `Attributes#to_hash` 167 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Specify your gem's dependencies in attributes_and_token_lists.gemspec. 5 | gemspec 6 | 7 | rails_version = ENV.fetch("RAILS_VERSION", "7.0") 8 | 9 | rails_constraint = if rails_version == "main" 10 | {github: "rails/rails"} 11 | else 12 | "~> #{rails_version}.0" 13 | end 14 | 15 | gem "rails", rails_constraint 16 | gem "sprockets-rails" 17 | gem "net-smtp" 18 | gem "capybara" 19 | gem "action_dispatch-testing-integration-capybara", 20 | github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.0", 21 | require: "action_dispatch/testing/integration/capybara/minitest" 22 | 23 | group :development do 24 | gem "sqlite3" 25 | end 26 | 27 | group :development, :test do 28 | gem "standard" 29 | end 30 | 31 | # To use a debugger 32 | # gem 'byebug', group: [:development, :test] 33 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Sean Doyle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AttributesAndTokenLists 2 | 3 | Transform `Hash` arguments into composable groups of HTML attributes 4 | 5 | --- 6 | 7 | ## Usage 8 | 9 | ### `tag.attributes` 10 | 11 | Installing `AttributesAndTokenLists` extends Action View's `tag.attributes` to 12 | return an instance of `ActionView::Attributes`. 13 | 14 | `ActionView::Attributes` are `Hash`-like objects with two distinguishing 15 | abilities: 16 | 17 | 1. They know how to serialize themselves into valid HTML attributes: 18 | 19 | ```ruby 20 | attributes = tag.attributes class: "border rounded-full p-4 aria-[expanded=false]:text-gray-500", 21 | aria: { 22 | controls: "a_disclosure", 23 | expanded: false 24 | }, 25 | data: { 26 | controller: "disclosure", 27 | action: "click->disclosure#toggle", 28 | disclosure_element_outlet: "#a_disclosure" 29 | } 30 | 31 | attributes.to_s 32 | # => class="border rounded-full p-4 aria-[expanded=false]:text-gray-500" aria-controls="a_disclosure" aria-expanded="false" data-controller="disclosure" data-action="click->disclosure#toggle" data-disclosure-element-outlet="#a_disclosure" 33 | ``` 34 | 35 | 2. They know how to deeply merge into attributes that are token lists: 36 | 37 | ```ruby 38 | border = tag.attribute class: "border rounded-full" 39 | padding = tag.attribute class: "p-4" 40 | 41 | attributes = border.merge(padding).merge(class: "a-special-class") 42 | 43 | attributes.to_h 44 | # => { class: "border rounded-full p-4 a-special-class" } 45 | ``` 46 | 47 | ### `with_options` 48 | 49 | Since `ActionView::Attributes` instances are `Hash`-like, they're compatible 50 | with [Object#with_options][]. Compose instances together to 51 | 52 | [Object#with_options]: https://edgeapi.rubyonrails.org/classes/Object.html#method-i-with_options 53 | 54 | ```ruby 55 | def focusable 56 | tag.attributes class: "focus:outline-none focus:ring-2" 57 | end 58 | 59 | def button 60 | tag.attributes class: "py-2 px-4 font-semibold shadow-md" 61 | end 62 | 63 | def primary 64 | tag.attributes class: "bg-black rounded-lg text-white hover:bg-yellow-300 focus:ring-yellow-300 focus:ring-opacity-75" 65 | end 66 | 67 | def primary_button(...) 68 | tag.with_options([focusable, button, primary].reduce(:merge)) 69 | end 70 | 71 | primary_button.button "Save", class: "uppercase" 72 | #=> 75 | 76 | primary_button.a "Cancel", href: "/", class: "uppercase" 77 | #=> 78 | #=> Cancel 79 | #=> 80 | ``` 81 | 82 | Attribute support isn't limited to `class:`. Declare composable ARIA- and 83 | Stimulus-aware attributes with `aria:` and `data:` keys: 84 | 85 | ```ruby 86 | def disclosure(controls: nil, expanded: false) 87 | tag.attributes aria: {controls:, expanded:}, 88 | data: {controller: "disclosure", action: "click->disclosure#toggle", disclosure_element_outlet: ("#" + controls if controls)} 89 | end 90 | 91 | def primary_disclosure_button(controls: nil, expanded: false) 92 | tag.with_options([focusable, button, primary, disclosure(controls:, expanded:)].reduce(:merge)) 93 | end 94 | 95 | primary_disclosure_button.button "Toggle", controls: "a_disclosure", expanded: true 96 | #=> 101 | 102 | primary_disclosure_button.summary "Toggle" 103 | #=> data-controller="disclosure" data-action="click->disclosure#toggle"> 105 | #=> Toggle 106 | #=> 107 | ``` 108 | 109 | ### `#with_attributes` 110 | 111 | Inspired by `#with_options`, the `#with_attributes` is a short-hand method that 112 | combines any number of `Hash`-like arguments into an `ActionView::Attributes` 113 | instance, then passes that along as an argument to `#with_options`. 114 | 115 | The `#with_attributes` method available both as an Action View helper method and 116 | as `tag` instance method. 117 | 118 | ```ruby 119 | with_attributes {class: "border rounded-full"}, {class: "p-4"}, class: "focus:outline-none focus:ring" do |styled| 120 | styled.link_to "A link", "/a-link" 121 | # => A link 122 | 123 | styled.button_tag "A button", type: "button" 124 | # => 125 | end 126 | 127 | builder = tag.with_attributes {class: "border rounded-full"}, {class: "p-4"}, class: "focus:outline-none focus:ring" 128 | 129 | builder.a "A link", href: "/a-link" 130 | # => A link 131 | 132 | builder.button_tag "A button", type: "button" 133 | # => 134 | ``` 135 | 136 | ## Installation 137 | Add this line to your application's Gemfile: 138 | 139 | ```ruby 140 | gem 'attributes_and_token_lists', github: "seanpdoyle/attributes_and_token_lists", branch: "main" 141 | ``` 142 | 143 | And then execute: 144 | ```bash 145 | $ bundle 146 | ``` 147 | 148 | Or install it yourself as: 149 | ```bash 150 | $ gem install attributes_and_token_lists 151 | ``` 152 | 153 | ## Contributing 154 | Contribution directions go here. 155 | 156 | ## License 157 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 158 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) 4 | load "rails/tasks/engine.rake" 5 | 6 | load "rails/tasks/statistics.rake" 7 | 8 | require "bundler/gem_tasks" 9 | 10 | require "rake/testtask" 11 | require "standard/rake" 12 | 13 | Rake::TestTask.new(:test) do |t| 14 | t.libs << "test" 15 | t.pattern = "test/**/*_test.rb" 16 | t.verbose = false 17 | end 18 | 19 | task default: :test 20 | -------------------------------------------------------------------------------- /app/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/app/helpers/.keep -------------------------------------------------------------------------------- /app/helpers/attributes_and_token_lists/application_helper.rb: -------------------------------------------------------------------------------- 1 | module AttributesAndTokenLists::ApplicationHelper 2 | # Inspired by `Object#with_options`, when the `with_attributes` helper 3 | # is called with a block, 4 | # it yields a block argument that merges options into a base set of 5 | # attributes. For example: 6 | # 7 | # with_attributes class: "border rounded-sm p-4" do |styled| 8 | # styled.link_to "I'm styled!", "/" 9 | # #=> I'm styled! 10 | # end 11 | # 12 | # When the block is omitted, the object that would be the block 13 | # parameter is returned: 14 | # 15 | # styled = with_attributes class: "border rounded-sm p-4" 16 | # styled.link_to "I'm styled!", "/" 17 | # #=> I'm styled! 18 | # 19 | # To change the receiver from the view context, invoke 20 | # with_attributes an the instance returned from another 21 | # with_attributes call: 22 | # 23 | # button = with_attributes class: "border rounded-sm p-4" 24 | # button.link_to "I have a border", "/" 25 | # #=> I have a border 26 | # 27 | # primary = button.with_attributes class: "text-red-500 border-red-500" 28 | # primary.link_to "I have a red border", "/" 29 | # #=> I have a red border 30 | # 31 | # secondary = button.with_attributes class: "text-blue-500 border-blue-500" 32 | # secondary.link_to "I have a blue border", "/" 33 | # #=> I have a blue border 34 | # 35 | ruby2_keywords def with_attributes(*values, &block) 36 | with_options(tag.attributes(*values), &block) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /attributes_and_token_lists.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/attributes_and_token_lists/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "attributes_and_token_lists" 5 | spec.version = AttributesAndTokenLists::VERSION 6 | spec.authors = ["Sean Doyle"] 7 | spec.email = ["sean.p.doyle24@gmail.com"] 8 | spec.homepage = "https://github.com/seanpdoyle/attributes_and_token_lists" 9 | spec.summary = "Change Action View's `token_lists` and `class_names` helpers to return instances 10 | of `TokenList`, and `tag.attributes` helpers to return instances of 11 | `Attributes`." 12 | spec.description = spec.summary 13 | spec.license = "MIT" 14 | 15 | spec.metadata["homepage_uri"] = spec.homepage 16 | spec.metadata["source_code_uri"] = "https://github.com/seanpdoyle/attributes_and_token_lists" 17 | spec.metadata["changelog_uri"] = "https://github.com/seanpdoyle/attributes_and_token_lists/blob/main/CHANGELOG.md" 18 | 19 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 20 | 21 | spec.add_dependency "rails", ">= 6.1.3.1" 22 | end 23 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/attributes_and_token_lists/engine', __dir__) 7 | APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 12 | 13 | require "rails/all" 14 | require "rails/engine/commands" 15 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | end 3 | -------------------------------------------------------------------------------- /lib/action_view/attributes.rb: -------------------------------------------------------------------------------- 1 | class ActionView::Attributes < DelegateClass(Hash) 2 | cattr_accessor :token_lists, default: Set.new 3 | 4 | def initialize(view_context, value = {}, &serializer) 5 | super(value) 6 | @view_context = view_context 7 | @serializer = serializer 8 | end 9 | 10 | def dup 11 | ActionView::Attributes.new(@view_context, super, &@serializer) 12 | end 13 | 14 | def merge(values, &block) 15 | dup.merge!(values, &block) 16 | end 17 | alias_method :deep_merge, :merge 18 | 19 | def merge!(values, &block) 20 | merge_conflicts = block || proc do |name, left, right| 21 | if token_list?(name) 22 | token_list(left, right) 23 | elsif left.respond_to?(:merge) && right.respond_to?(:to_h) 24 | deep_merge_token_lists left, right, namespace: name 25 | else 26 | right 27 | end 28 | end 29 | 30 | super(values, &merge_conflicts) 31 | 32 | self 33 | end 34 | alias_method :deep_merge!, :merge! 35 | 36 | def to_s 37 | if @serializer 38 | yield_self(&@serializer) 39 | else 40 | super 41 | end 42 | end 43 | 44 | private 45 | 46 | delegate :token_list, to: :@view_context 47 | 48 | def deep_merge_token_lists(attributes, overrides, namespace:) 49 | attributes.merge(overrides.to_h) do |name, left, right| 50 | if token_list?("#{namespace}-#{name.to_s.dasherize}") 51 | token_list(left, right) 52 | else 53 | right 54 | end 55 | end 56 | end 57 | 58 | def token_list?(name) 59 | name.in?(token_lists) || name.to_s.in?(token_lists) || matches_token_list_pattern?(name.to_s) 60 | end 61 | 62 | def matches_token_list_pattern?(name) 63 | token_list_patterns.any? { |pattern| pattern.match?(name.to_s.dasherize) } 64 | end 65 | 66 | def token_list_patterns 67 | token_lists.select { |token_list| token_list.is_a?(Regexp) } 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/action_view/helpers/tag_helper/tag_builder.rb: -------------------------------------------------------------------------------- 1 | class ActionView::Helpers::TagHelper::TagBuilder 2 | # === Passing a single Hash argument 3 | # 4 | # > 5 | # # => 6 | # 7 | # === Passing multiple Hash arguments 8 | # 9 | # Passing multiple Hash arguments will be deep merged from left to right into a single Hash: 10 | # 11 | # > 12 | # # => 13 | # 14 | # Hash arguments can be mixed with keyword arguments: 15 | # 16 | # > 17 | # # => 18 | # 19 | # When called outside of a rendering context, tag.attributes 20 | # will return a Hash-like object that knows how to render 21 | # itself to HTML: 22 | # 23 | # primary = { class: "bg-red-500 text-white" } 24 | # large = { class: "text-lg p-4" } 25 | # 26 | # button_tag "Click me!", tag.attributes(primary, large) 27 | # # => 28 | # 29 | # tag.button "Click me!", id: "cta", **tag.attributes(primary, large) 30 | # # => 31 | # 32 | # === Token list support 33 | # 34 | # Attribute merging will account for token lists attributes (like class and aria-labelledby) by combining values from left to right 35 | # 36 | # > 37 | # # => 38 | # 39 | # To override token list merging for an attribute, pass its name with a `!` suffix: 40 | # 41 | # attributes = tag.attributes({ class: "default" }, { class!: "first-override" }, class!: "second-override") 42 | # attributes.to_h # => { class: "second-override" } 43 | # attributes.to_s # => "class=\"second-override\"" 44 | # 45 | # Keys that end with ! are only temporary. Accessing 46 | # attributes[:class!] will return nil in the code sample 47 | # above. 48 | # 49 | # === Configuring token list support 50 | # 51 | # To treat addition attributes as token lists, add values to the config.action_view.token_lists value: 52 | # 53 | # config.action_view.token_lists << "data-action" 54 | # config.action_view.token_lists << "data-controller" 55 | # config.action_view.token_lists << /data-(.*)-target/ 56 | # 57 | ruby2_keywords def attributes(*hashes) 58 | attributes = ActionView::Attributes.new(@view_context) do |value| 59 | tag_options(value).to_s.strip.html_safe 60 | end 61 | 62 | hashes.tap(&:compact!).reduce(attributes, :merge!) 63 | end 64 | 65 | # Inspired by `Object#with_options`, when the `with_attributes` helper is 66 | # called with a block, it forwards the other arguments to 67 | # `ActionView::Helpers::TagHelper::TagBuilder#attributes`, then yields that 68 | # value to the block argument that merges options into a base set of 69 | # attributes. For example: 70 | # 71 | # border = { class: "border rounded-sm p-4" } 72 | # spacing = { class: "p-4" } 73 | # 74 | # tag.with_attributes border, spacing do |styled| 75 | # styled.button "I'm red!", class: "text-red-500" 76 | # #=> 77 | # end 78 | # 79 | # When the block is omitted, the object that would be the block 80 | # parameter is returned: 81 | # 82 | # border = { class: "border rounded-sm p-4" } 83 | # spacing = { class: "p-4" } 84 | # 85 | # styled = tag.with_attributes border, spacing 86 | # styled.button "I'm styled!", class: "text-red-500" 87 | # #=> 88 | # 89 | # To change the receiver from the view context, invoke 90 | # with_attributes an the instance returned from another 91 | # with_attributes call: 92 | # 93 | # button = tag.with_attributes class: "border rounded-sm p-4" 94 | # button.a "I have a border", href: "/" 95 | # #=> I have a border 96 | # 97 | # primary = button.with_attributes class: "text-red-500 border-red-500" 98 | # primary.a "I have a red border", href: "/" 99 | # #=> I have a red border 100 | # 101 | # secondary = button.with_attributes class: "text-blue-500 border-blue-500" 102 | # secondary.a "I have a blue border", href: "/" 103 | # #=> I have a blue border 104 | # 105 | ruby2_keywords def with_attributes(*values, &block) 106 | with_options(attributes(*values), &block) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/attributes_and_token_lists.rb: -------------------------------------------------------------------------------- 1 | require "attributes_and_token_lists/version" 2 | require "attributes_and_token_lists/engine" 3 | require "attributes_and_token_lists/tag_builder" 4 | 5 | module AttributesAndTokenLists 6 | mattr_accessor(:config) { ActiveSupport::OrderedOptions.new } 7 | 8 | def self.builder(name, &block) 9 | instance = config.builders[name] = Class.new(TagBuilder) 10 | 11 | if block.present? 12 | block.arity.zero? ? instance.instance_exec(&block) : instance.yield_self(&block) 13 | end 14 | end 15 | 16 | def self.define_builder_helper_methods(helpers) 17 | config.builders.each do |name, builder| 18 | helpers.define_method name do 19 | builder.new(self) 20 | end 21 | end 22 | 23 | helpers 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/attributes_and_token_lists/engine.rb: -------------------------------------------------------------------------------- 1 | module AttributesAndTokenLists 2 | class Engine < ::Rails::Engine 3 | config.attributes_and_token_lists = ActiveSupport::OrderedOptions.new 4 | config.attributes_and_token_lists.builders = {} 5 | 6 | initializer "attributes_and_token_lists.core_ext" do 7 | if Rails.version < "7.0" 8 | require "attributes_and_token_lists/object_backports" 9 | end 10 | end 11 | 12 | ActiveSupport.on_load :action_view do 13 | require "action_view/attributes" 14 | require "action_view/helpers/tag_helper/tag_builder" 15 | 16 | ActionView::Attributes.class_eval do 17 | self.token_lists = [ 18 | "class", 19 | "rel", 20 | "aria-controls", 21 | "aria-describedby", 22 | "aria-details", 23 | "aria-dropeffect", 24 | "aria-flowto", 25 | "aria-keyshortcuts", 26 | "aria-labelledby", 27 | "aria-owns", 28 | "aria-relevant", 29 | "data-action", 30 | "data-controller", 31 | /data-(.*)-target/ 32 | ].to_set 33 | 34 | def aria(values) 35 | merge(aria: values) 36 | end 37 | 38 | def data(values) 39 | merge(data: values) 40 | end 41 | end 42 | end 43 | 44 | config.to_prepare do 45 | AttributesAndTokenLists.config = ::Rails.configuration.attributes_and_token_lists 46 | 47 | ActiveSupport.run_load_hooks :attributes_and_token_lists, AttributesAndTokenLists 48 | 49 | AttributesAndTokenLists.define_builder_helper_methods(ActionView::Base) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/attributes_and_token_lists/object_backports.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/object/with_options" 2 | 3 | class Object 4 | alias_method :__original_with_options, :with_options 5 | 6 | def with_options(options, &block) 7 | if block.nil? 8 | options_merger = nil 9 | __original_with_options(options) { |object| options_merger = object } 10 | options_merger 11 | else 12 | __original_with_options(options, &block) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/attributes_and_token_lists/tag_builder.rb: -------------------------------------------------------------------------------- 1 | module AttributesAndTokenLists 2 | class TagBuilder 3 | class_attribute :tag_name, instance_accessor: false, default: :div 4 | 5 | def self.base(name, tag_name: self.tag_name, **defaults, &block) 6 | if block.present? 7 | builder_class = Class.new(self) do 8 | self.tag_name = tag_name 9 | 10 | block.arity.zero? ? instance_exec(&block) : yield_self(&block) 11 | end 12 | 13 | if name.in?(instance_methods) && (existing_method = instance_method(name)) 14 | raise <<~ERROR 15 | Cannot define "#{existing_method.name}", it's already defined by #{existing_method.source_location}" 16 | ERROR 17 | else 18 | define_method name do 19 | builder_class.new(view_context, defaults, tag_name: tag_name) 20 | end 21 | end 22 | else 23 | variant(name, tag_name: tag_name, **defaults) 24 | end 25 | end 26 | 27 | def self.variant(name, tag_name: self.tag_name, **defaults) 28 | if name.in?(instance_methods) && (existing_method = instance_method(name)) 29 | raise <<~ERROR 30 | Cannot define "#{existing_method.name}", it's already defined by #{existing_method.source_location}" 31 | ERROR 32 | else 33 | define_method name do |*arguments, **options, &block| 34 | tag_builder = as(tag_name).merge!(defaults) 35 | 36 | if arguments.none? && options.none? && block.nil? 37 | tag_builder 38 | else 39 | tag_builder.tag(*arguments, **options, &block) 40 | end 41 | end 42 | end 43 | end 44 | 45 | def initialize(view_context, *values, tag_name: self.class.tag_name) 46 | self.view_context = view_context 47 | self.attributes = view_context.tag.attributes(*values) 48 | self.tag_name = tag_name 49 | end 50 | 51 | def merge!(...) 52 | tap { attributes.merge!(...) } 53 | end 54 | 55 | def merge(...) 56 | dup.merge!(...) 57 | end 58 | alias_method :call, :merge 59 | 60 | def with(*names) 61 | variants = names.tap(&:compact!).tap(&:flatten!).map { |name| public_send(name) } 62 | 63 | variants.reduce(dup) { |combined, variant| combined.merge!(variant.to_h) } 64 | end 65 | 66 | def as(tag_name) 67 | tap { self.tag_name = tag_name } 68 | end 69 | 70 | def tag(*arguments, **options, &block) 71 | tag_builder = view_context.tag.with_options(attributes) 72 | 73 | if arguments.none? && options.none? && block.nil? 74 | tag_builder 75 | else 76 | tag_builder.public_send(tag_name, *arguments, **options, &block) 77 | end 78 | end 79 | 80 | def to_s 81 | attributes.to_s 82 | end 83 | 84 | def dup 85 | self.class.new(view_context, attributes.dup, tag_name: tag_name) 86 | end 87 | 88 | def method_missing(name, ...) 89 | receiver = 90 | if attributes.respond_to?(name) 91 | attributes 92 | elsif view_context.respond_to?(name) 93 | view_context.with_options(attributes) 94 | else 95 | view_context.tag.with_options(attributes) 96 | end 97 | 98 | receiver.public_send(name, ...) 99 | end 100 | 101 | def respond_to_missing?(name, include_private = false) 102 | [attributes, view_context, view_context.tag].any? { |receiver| receiver.respond_to?(name, include_private) } 103 | end 104 | 105 | private 106 | 107 | attr_accessor :attributes, :tag_name, :view_context 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/attributes_and_token_lists/version.rb: -------------------------------------------------------------------------------- 1 | module AttributesAndTokenLists 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/attributes_and_token_lists_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :attributes_and_token_lists do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /test/attributes_and_token_lists/tag_builder_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "capybara/minitest" 3 | 4 | class AttributesAndTokenLists::TagBuilderTest < ActionView::TestCase 5 | include Capybara::Minitest::Assertions 6 | 7 | test "AttributesAndTokenLists.define declares helper" do 8 | define_builder_helper_method :test_builder 9 | define_builder_helper_method :another_builder 10 | 11 | assert view.respond_to?(:test_builder), "declares helper methods" 12 | assert view.respond_to?(:another_builder), "declares helper methods" 13 | end 14 | 15 | test "definitions yield the builder as an argument" do 16 | define_builder_helper_method :builder do |instance| 17 | instance.base :rounded, class: "rounded-full" 18 | end 19 | 20 | render inline: <<~ERB 21 | <%= builder.rounded.tag "Content" %> 22 | ERB 23 | 24 | assert_css "div", class: "rounded-full", text: "Content" 25 | end 26 | 27 | test "definitions can omit the builder argument from the block" do 28 | define_builder_helper_method :builder do 29 | base :rounded, class: "rounded-full" 30 | end 31 | 32 | render inline: <<~ERB 33 | <%= builder.rounded.tag "Content" %> 34 | ERB 35 | 36 | assert_css "div", class: "rounded-full", text: "Content" 37 | end 38 | 39 | test "definitions can declare a default tag with the tag_name: option" do 40 | define_builder_helper_method :builder do 41 | base :button, tag_name: :button, class: "rounded-full" 42 | end 43 | 44 | render inline: <<~ERB 45 | <%= builder.button.tag "Submit" %> 46 | <%= builder.button.button_tag "Submit" %> 47 | ERB 48 | 49 | assert_button "Submit", class: "rounded-full", count: 2 50 | end 51 | 52 | test "definitions can define other variants" do 53 | define_builder_helper_method :builder do 54 | base :button, tag_name: :button, class: "rounded-full" do 55 | variant :primary, class: "bg-green-500" 56 | end 57 | end 58 | 59 | render inline: <<~ERB 60 | <%= builder.button.tag "Base" %> 61 | <%= builder.button.primary.tag "Primary" %> 62 | <%= builder.button.primary.button_tag "Primary" %> 63 | ERB 64 | 65 | assert_button "Base", class: %w[rounded-full] 66 | assert_button "Primary", class: %w[rounded-full bg-green-500], count: 2 67 | end 68 | 69 | test "variants can be combined by calls to #with" do 70 | define_builder_helper_method :builder do 71 | base :button, tag_name: :button do 72 | variant :primary, class: "bg-green-500" 73 | variant :rounded, class: "rounded-full" 74 | end 75 | end 76 | 77 | render inline: <<~ERB 78 | <%= builder.button.with(nil).tag "Base" %> 79 | <%= builder.button.with(:primary).tag "Primary" %> 80 | <%= builder.button.with(:primary, :rounded).button_tag "Primary Rounded" %> 81 | <%= builder.button.with([:primary, :rounded]).button_tag "Primary Rounded Array" %> 82 | ERB 83 | 84 | assert_button "Base", class: %w[] 85 | assert_button "Primary", exact: true, class: %w[bg-green-500], count: 1 86 | assert_button "Primary Rounded", exact: true, class: %w[bg-green-500 rounded-full], count: 1 87 | assert_button "Primary Rounded Array", exact: true, class: %w[bg-green-500 rounded-full], count: 1 88 | end 89 | 90 | test "defined attributes can render with content" do 91 | define_builder_helper_method :builder do 92 | base :button, tag_name: :button 93 | end 94 | 95 | render inline: <<~ERB 96 | <%= builder.button.tag "Submit" %> 97 | ERB 98 | 99 | assert_button "Submit" 100 | end 101 | 102 | test "defined attributes can render without content" do 103 | define_builder_helper_method :builder do 104 | base :input, tag_name: :input do 105 | variant :text, type: "text" 106 | variant :submit, type: "submit" 107 | end 108 | end 109 | 110 | render inline: <<~ERB 111 | <%= builder.input.tag value: "Default" %> 112 | <%= builder.input.text.tag value: "Text" %> 113 | <%= builder.input.submit.tag value: "Submit"%> 114 | ERB 115 | 116 | assert_field(with: "Default", exact: true, count: 1) { _1["type"].nil? } 117 | assert_field(type: "text", with: "Text", count: 1) 118 | assert_button("Submit", type: "submit", count: 1) 119 | end 120 | 121 | test "defined attributes can render with overrides" do 122 | define_builder_helper_method :builder do 123 | base :button, tag_name: :button, type: "submit" 124 | end 125 | 126 | render inline: <<~ERB 127 | <%= builder.button.tag "Submit" %> 128 | <%= builder.button.tag "Reset", type: "reset" %> 129 | ERB 130 | 131 | assert_button "Submit", type: "submit" 132 | assert_button "Reset", type: "reset" 133 | end 134 | 135 | test "defined attributes can render as other tags" do 136 | define_builder_helper_method :builder do 137 | base :button, tag_name: :button, class: "rounded-full" 138 | end 139 | 140 | render inline: <<~ERB 141 | <%= builder.button.tag "A button" %> 142 | <%= builder.button.tag.input value: "An input", type: "button" %> 143 | <%= builder.button.tag.a "A link", href: "#" %> 144 | ERB 145 | 146 | assert_button "A button", class: "rounded-full" 147 | assert_field with: "An input", class: "rounded-full", type: "button" 148 | assert_link "A link", class: "rounded-full", href: "#" 149 | end 150 | 151 | test "defined attributes splat into Action View helpers" do 152 | define_builder_helper_method :builder do 153 | base :button, tag_name: :button, class: "rounded-full" 154 | end 155 | 156 | render inline: <<~ERB 157 | <%= form_with scope: :post, url: "/" do |form| %> 158 | <%= form.button "As Options", builder.button %> 159 | <%= form.button "Chained Call", builder.button.(class: "btn") %> 160 | <% end %> 161 | ERB 162 | 163 | assert_css "form" do 164 | assert_button "As Options", class: %(rounded-full), type: "submit", count: 1 165 | assert_button "Chained Call", class: %(rounded-full btn), type: "submit", count: 1 166 | end 167 | end 168 | 169 | test "AttributesAndTokenLists::ApplicationHelper#with_attributes accepts a TagBuilder instance" do 170 | define_builder_helper_method :builder do 171 | base :button, tag_name: :button, class: "rounded-full" 172 | end 173 | 174 | render inline: <<~ERB 175 | <% with_attributes builder.button, class: "btn" do |styled| %> 176 | <%= styled.button_tag "Submit" %> 177 | <% end %> 178 | ERB 179 | 180 | assert_button "Submit", class: %w[rounded-full btn], type: "submit" 181 | end 182 | 183 | test "AttributesAndTokenLists::ApplicationHelper#to_s delegates to ActionView::Attributes" do 184 | define_builder_helper_method :builder do 185 | base :button, tag_name: :button, class: "rounded-full" 186 | end 187 | 188 | rendered = render inline: "<%= builder.button %>" 189 | 190 | assert_equal %(class="rounded-full"), rendered 191 | end 192 | 193 | test "cannot name a variant after an existing method" do 194 | collision = :with 195 | 196 | exception = assert_raises do 197 | define_builder_helper_method(:builder) { base collision } 198 | end 199 | 200 | assert_includes exception.message, %(Cannot define "#{collision}", it's already defined) 201 | end 202 | 203 | test "cannot name a base after an existing method" do 204 | collision = :with 205 | exception = assert_raises do 206 | define_builder_helper_method :builder do 207 | base(:button) { variant collision } 208 | end 209 | end 210 | 211 | assert_includes exception.message, %(Cannot define "#{collision}", it's already defined) 212 | end 213 | 214 | def page 215 | @page ||= Capybara.string(rendered) 216 | end 217 | 218 | def define_builder_helper_method(name, &block) 219 | AttributesAndTokenLists.builder(name, &block) 220 | view.extend(AttributesAndTokenLists.define_builder_helper_methods(Module.new)) 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /test/attributes_and_token_lists_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AttributesAndTokenListsTest < ActiveSupport::TestCase 4 | test "it has a version number" do 5 | assert AttributesAndTokenLists::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/controllers/examples_controller.rb: -------------------------------------------------------------------------------- 1 | class ExamplesController < ApplicationController 2 | def create 3 | fail unless Rails.env.test? 4 | 5 | render inline: params[:template] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "action_controller/railtie" 4 | require "action_view/railtie" 5 | require "rails/test_unit/railtie" 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | require "attributes_and_token_lists" 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | config.load_defaults Rails::VERSION::STRING.to_f 15 | 16 | # Configuration for the application, engines, and railties goes here. 17 | # 18 | # These settings can be overridden in specific environments using the files 19 | # in config/environments, which are processed later. 20 | # 21 | # config.time_zone = "Central Time (US & Canada)" 22 | # config.eager_load_paths << Rails.root.join("extras") 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) 3 | 4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join("tmp", "caching-dev.txt").exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Raise an error on page load if there are pending migrations. 51 | config.active_record.migration_error = :page_load 52 | 53 | # Highlight code that triggered database queries in logs. 54 | config.active_record.verbose_query_logs = true 55 | 56 | # Debug mode disables concatenation and preprocessing of assets. 57 | # This option may cause significant delays in view rendering with a large 58 | # number of complex assets. 59 | config.assets.debug = true 60 | 61 | # Suppress logger output for asset requests. 62 | config.assets.quiet = true 63 | 64 | # Raises error for missing translations. 65 | # config.i18n.raise_on_missing_translations = true 66 | 67 | # Annotate rendered view with file names. 68 | # config.action_view.annotate_rendered_view_with_filenames = true 69 | 70 | # Use an evented file watcher to asynchronously detect changes in source code, 71 | # routes, locales, etc. This feature depends on the listen gem. 72 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 73 | 74 | # Uncomment if you wish to allow Action Cable access from any origin. 75 | # config.action_cable.disable_request_forgery_protection = true 76 | end 77 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = 'http://assets.example.com' 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 38 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = 'wss://example.com/cable' 46 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [:request_id] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "dummy_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Send deprecation notices to registered listeners. 76 | config.active_support.deprecation = :notify 77 | 78 | # Log disallowed deprecations. 79 | config.active_support.disallowed_deprecation = :log 80 | 81 | # Tell Active Support which deprecation messages to disallow. 82 | config.active_support.disallowed_deprecation_warnings = [] 83 | 84 | # Use default logging formatter so that PID and timestamp are not suppressed. 85 | config.log_formatter = ::Logger::Formatter.new 86 | 87 | # Use a different logger for distributed setups. 88 | # require "syslog/logger" 89 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 90 | 91 | if ENV["RAILS_LOG_TO_STDOUT"].present? 92 | logger = ActiveSupport::Logger.new($stdout) 93 | logger.formatter = config.log_formatter 94 | config.logger = ActiveSupport::TaggedLogging.new(logger) 95 | end 96 | 97 | # Do not dump schema after migrations. 98 | config.active_record.dump_schema_after_migration = false 99 | 100 | # Inserts middleware to perform automatic connection switching. 101 | # The `database_selector` hash is used to pass options to the DatabaseSelector 102 | # middleware. The `delay` is used to determine how long to wait after a write 103 | # to send a subsequent read to the primary. 104 | # 105 | # The `database_resolver` class is used by the middleware to determine which 106 | # database is appropriate to use based on the time delay. 107 | # 108 | # The `database_resolver_context` class is used by the middleware to set 109 | # timestamps for the last write to the primary. The resolver uses the context 110 | # class timestamps to determine how long to wait before reading from the 111 | # replica. 112 | # 113 | # By default Rails will store a last write timestamp in the session. The 114 | # DatabaseSelector middleware is designed as such you can define your own 115 | # strategy for connection switching and pass that into the middleware through 116 | # these configuration options. 117 | # config.active_record.database_selector = { delay: 2.seconds } 118 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 119 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 120 | end 121 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | config.cache_classes = true 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | config.cache_store = :null_store 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = false 31 | 32 | # Disable request forgery protection in test environment. 33 | config.action_controller.allow_forgery_protection = false 34 | 35 | # Store uploaded files on the local file system in a temporary directory. 36 | config.active_storage.service = :test 37 | 38 | # Print deprecation notices to the stderr. 39 | config.active_support.deprecation = :stderr 40 | 41 | # Raise exceptions for disallowed deprecations. 42 | config.active_support.disallowed_deprecation = :raise 43 | 44 | # Tell Active Support which deprecation messages to disallow. 45 | config.active_support.disallowed_deprecation_warnings = [] 46 | 47 | # Raises error for missing translations. 48 | # config.i18n.raise_on_missing_translations = true 49 | 50 | # Annotate rendered view with file names. 51 | # config.action_view.annotate_rendered_view_with_filenames = true 52 | end 53 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/attributes_and_token_lists.html.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load :attributes_and_token_lists do 2 | builder :ui do 3 | base :button, tag_name: :button, class: "text-white p-4 focus:outline-none focus:ring" do 4 | variant :primary, class: "bg-green-500" 5 | variant :secondary, class: "bg-blue-500" 6 | variant :tertiary, class: "bg-black" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT", 3000) 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 3 | resources :examples, only: :create 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpdoyle/attributes_and_token_lists/349445c8322f84faa06c085438d12e750ae4eab1/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/helpers/attributes_and_token_lists/application_helper_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AttributesAndTokenLists::ApplicationHelperTest < ActionView::TestCase 4 | test "tag.attributes without arguments returns blank Attributes" do 5 | attributes = tag.attributes 6 | 7 | assert_empty attributes.to_h 8 | end 9 | 10 | test "tag.attributes with nil returns blank Attributes" do 11 | attributes = tag.attributes(nil) 12 | 13 | assert_empty attributes.to_h 14 | end 15 | 16 | test "tag.attributes merges into token list attributes" do 17 | attributes = tag.attributes(class: "one").merge(class: "one two") 18 | 19 | assert_equal %(class="one two"), attributes.to_s 20 | end 21 | 22 | test "tag.attributes can be splatted" do 23 | attributes = {id: 1, **tag.attributes(class: "one")} 24 | 25 | assert_equal({id: 1, class: "one"}, attributes) 26 | end 27 | 28 | test "tag.attributes merges like a Hash of attributes" do 29 | attributes = tag.attributes(id: 1, class: "one").merge(hidden: true) 30 | 31 | assert_equal %(id="1" class="one" hidden="hidden"), attributes.to_s 32 | end 33 | 34 | test "tag.attributes merges variable arguments" do 35 | attributes = tag.attributes({id: 1, class: "one"}, {hidden: true}) 36 | 37 | assert_equal %(id="1" class="one" hidden="hidden"), attributes.to_s 38 | end 39 | 40 | test "tag.attributes merges variable arguments, with the last key-value pairs serving as overrides" do 41 | attributes = tag.attributes({type: nil}, {type: "button"}, type: "reset") 42 | 43 | assert_equal %(type="reset"), attributes.to_s 44 | end 45 | 46 | test "tag.attributes merges variable arguments, with a final override" do 47 | left = tag.attributes(id: 1) 48 | right = tag.attributes(class: "one") 49 | 50 | attributes = tag.attributes(left, right, class: "two") 51 | 52 | assert_equal %(id="1" class="one two"), attributes.to_s 53 | end 54 | 55 | test "tag.attributes combines token list attributes" do 56 | attributes = tag.attributes(class: "one").merge(class: "one two") 57 | 58 | assert_equal %(class="one two"), attributes.to_s 59 | end 60 | 61 | test "tag.attributes.aria deeply merges" do 62 | assert_equal %(aria-describedby="one two three"), tag.attributes(aria: {describedby: "one"}).aria(describedby: "two").aria(describedby: "three").to_s 63 | end 64 | 65 | test "tag.attributes.data deeply merges" do 66 | assert_equal %(data-controller="one two three"), tag.attributes(data: {controller: "one"}).data(controller: "two").data(controller: "three").to_s 67 | end 68 | 69 | test "tag.attributes deeply merges Hash attributes" do 70 | assert_equal %(data-controller="one two"), tag.attributes(data: {controller: "one"}).merge(data: {controller: "two"}).to_s 71 | end 72 | 73 | test "tag.attributes deeply merges token list attributes" do 74 | attributes = tag.attributes(data: {controller: "one"}).merge(data: {controller: "two"}) 75 | 76 | assert_equal %(data-controller="one two"), attributes.to_s 77 | end 78 | 79 | test "tag.attributes are serialized by the tag helper" do 80 | attributes = tag.attributes data: {controller: "one two"} 81 | 82 | assert_equal %(
), tag.form(**attributes) 83 | end 84 | 85 | test "tag.attributes are serialized by the tag helper when merged" do 86 | attributes = tag.attributes(class: "one").merge(class: "two") 87 | 88 | assert_equal %(
), tag.form(**attributes) 89 | end 90 | 91 | test "tag.attributes are serialized by the tag helper when deep merged" do 92 | attributes = tag.attributes(data: {controller: "one two"}).merge(data: {controller: "three"}) 93 | 94 | assert_equal %(
), tag.form(**attributes) 95 | end 96 | 97 | test "tag.attributes are compatible with tag.with_options calls" do 98 | attributes = tag.attributes(data: {controller: "one two"}).merge(data: {controller: "three"}) 99 | 100 | assert_equal %(
), tag.with_options(attributes).form 101 | end 102 | 103 | test "tag.attributes instances can chain view helper calls" do 104 | attributes = tag.attributes(class: "one two").merge(class: "three") 105 | 106 | assert_equal %(styled), with_options(attributes).link_to("styled", "/") 107 | end 108 | 109 | test "tag.with_attributes delegates to tag.attributes, then passes to tag.with_options" do 110 | one = {data: {controller: "one"}} 111 | two = {data: {controller: "two"}} 112 | 113 | assert_equal %(
), tag.with_attributes(one, two, data: {controller: "three"}).form 114 | end 115 | 116 | test "with_attributes can have options decorated onto it" do 117 | with_attributes class: "one two" do |styled| 118 | assert_equal %(styled), styled.link_to("styled", "/") 119 | assert_equal %(styled), styled.link_to("styled", "/", class: "three") 120 | end 121 | end 122 | 123 | test "with_attributes accepts an Attributes instance" do 124 | base = tag.attributes class: "one" 125 | styled = with_attributes base, class: "two" 126 | 127 | assert_equal %(test), styled.content_tag(:span, "test") 128 | end 129 | 130 | test "with_attributes can chain with_attributes calls decorate options further" do 131 | base = with_attributes class: "one two" 132 | styled = base.with_attributes class: "three" 133 | 134 | assert_equal %(styled), base.link_to("styled", "/") 135 | assert_equal %(styled), styled.link_to("styled", "/") 136 | assert_equal %(styled), styled.link_to("styled", "/", class: "four") 137 | end 138 | 139 | test "with_attributes can be chained off an Attributes instance" do 140 | attributes = tag.attributes class: "one" 141 | 142 | assert_equal %(test), tag.with_attributes(attributes, class: "two").span("test") 143 | assert_equal %(test), tag.with_attributes(attributes, class: "two").span("test") 144 | assert_equal %(test), tag.with_attributes(attributes, class: "two").span("test", class: "three") 145 | end 146 | 147 | test "with_attributes chained off an Attributes instance accepts an Attributes instance" do 148 | base = tag.attributes class: "one" 149 | attributes = tag.attributes class: "two" 150 | styled = with_attributes(base, attributes) 151 | 152 | assert_equal %(test), styled.content_tag(:span, "test") 153 | end 154 | 155 | test "with_attributes chained off an Attributes instance accepts instances returned from with_attributes" do 156 | base = with_attributes class: "one" 157 | styled = base.with_attributes class: "two" 158 | 159 | assert_equal %(test), styled.content_tag(:span, "test") 160 | end 161 | 162 | test "with_attributes chained off a TagBuilder instance accept Attributes instances" do 163 | styled = tag.attributes class: "one two" 164 | 165 | assert_equal %(styled), tag.with_attributes(styled).a("styled", href: "/") 166 | end 167 | 168 | test "with_attributes accepts an empty Hash" do 169 | assert_equal %(
), with_attributes({}).tag.div 170 | assert_equal %(
), with_attributes(**{}).tag.div 171 | assert_equal %(
), tag.with_attributes({}).div 172 | assert_equal %(
), tag.with_attributes(**{}).div 173 | end 174 | 175 | test "tag with arguments on an AttributeMerger instance invokes the method" do 176 | styled = with_attributes class: "one" 177 | 178 | assert_equal %(
), styled.tag(:br) 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /test/integration/helpers_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HelpersTest < ActionDispatch::IntegrationTest 4 | test "mixes helper into host Rails Application" do 5 | post examples_path, params: {template: <<~ERB} 6 | <%= tag.with_attributes({id: 1, class: "one"}, {hidden: true}, class: "two").button %> 7 | ERB 8 | 9 | assert_html_equal <<~HTML, response.body 10 | 11 | HTML 12 | end 13 | 14 | test "extends FormBuilder#with_options accepts tag.attributes (block)" do 15 | post examples_path, params: {template: <<~ERB} 16 | <%= form_with url: "/", authenticity_token: false, enforce_utf8: false do |form| %> 17 | <% form.with_options tag.attributes(class: "font-bold") do |special_form| %> 18 | <%= special_form.text_field :text, class: "text" %> 19 | <% end %> 20 | <% end %> 21 | ERB 22 | 23 | assert_html_equal <<~HTML, response.body 24 |
25 | 26 |
27 | HTML 28 | end 29 | 30 | test "extends FormBuilder#with_options accepts tag.attributes (instance)" do 31 | post examples_path, params: {template: <<~ERB} 32 | <%= form_with url: "/", authenticity_token: false, enforce_utf8: false do |form| %> 33 | <%= form.with_options(tag.attributes(class: "font-bold")).text_field :text, class: "text" %> 34 | <% end %> 35 | ERB 36 | 37 | assert_html_equal <<~HTML, response.body 38 |
39 | 40 |
41 | HTML 42 | end 43 | 44 | test "extends collection Builder#with_options accepts tag.attributes (block)" do 45 | post examples_path, params: {template: <<~ERB} 46 | <%= collection_check_boxes :record, :choice, ["a"], :to_s, :to_s, {include_hidden: false} do |builder| %> 47 | <% builder.with_options tag.attributes(class: "default") do |special_builder| %> 48 | <%= special_builder.check_box class: "override" %> 49 | <% end %> 50 | <% end %> 51 | ERB 52 | 53 | assert_html_equal <<~HTML, response.body 54 | 55 | HTML 56 | end 57 | 58 | test "extends collection Builder#with_options accepts tag.attributes (instance)" do 59 | post examples_path, params: {template: <<~ERB} 60 | <%= collection_check_boxes :record, :choice, ["a"], :to_s, :to_s, {include_hidden: false} do |builder| %> 61 | <%= builder.with_options(tag.attributes(class: "default")).check_box class: "override" %> 62 | <% end %> 63 | ERB 64 | 65 | assert_html_equal <<~HTML, response.body 66 | 67 | HTML 68 | end 69 | 70 | def assert_html_equal(expected, actual, *rest) 71 | assert_equal expected.squish, actual.squish, *rest 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/integration/tag_builder_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TagBuilderTest < ActionDispatch::IntegrationTest 4 | test "reads configuration from config/initializers" do 5 | post examples_path, params: {template: <<~ERB} 6 | <%= ui.tag.button "Unstyled" %> 7 | <%= ui.button.tag "Base" %> 8 | <%= ui.button.primary "Primary" %> 9 | <%= ui.button.secondary "Secondary" %> 10 | <%= ui.button.tertiary "Tertiary" %> 11 | <%= ui.button.primary.a "Primary", href: "#" %> 12 | <%= ui.button.primary.link_to "Primary", "#" %> 13 | <%= ui.button.with(:primary, :secondary, :tertiary).tag "All" %> 14 | ERB 15 | 16 | assert_button "Unstyled", class: %w[] 17 | assert_button "Base", class: %w[text-white p-4 focus:outline-none focus:ring] 18 | assert_button "Primary", class: %w[text-white p-4 focus:outline-none focus:ring bg-green-500] 19 | assert_button "Secondary", class: %w[text-white p-4 focus:outline-none focus:ring bg-blue-500] 20 | assert_button "Tertiary", class: %w[text-white p-4 focus:outline-none focus:ring bg-black] 21 | assert_link "Primary", class: %w[text-white p-4 focus:outline-none focus:ring bg-green-500], href: "#", count: 2 22 | assert_button "All", class: %w[text-white p-4 focus:outline-none focus:ring bg-green-500 bg-blue-500 bg-black] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require_relative "../test/dummy/config/environment" 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 6 | require "rails/test_help" 7 | 8 | # Load fixtures from the engine 9 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 10 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) 11 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path 12 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" 13 | ActiveSupport::TestCase.fixtures :all 14 | end 15 | --------------------------------------------------------------------------------