{"id":13878998,"url":"https://github.com/palkan/view_component-contrib","last_synced_at":"2025-05-14T15:06:02.609Z","repository":{"id":43060585,"uuid":"350361175","full_name":"palkan/view_component-contrib","owner":"palkan","description":"A collection of extension and developer tools for ViewComponent","archived":false,"fork":false,"pushed_at":"2025-01-29T23:53:44.000Z","size":157,"stargazers_count":390,"open_issues_count":4,"forks_count":24,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-12T02:56:00.099Z","etag":null,"topics":["hacktoberfest","rails","view-components"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/palkan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-22T13:50:47.000Z","updated_at":"2025-04-05T10:37:08.000Z","dependencies_parsed_at":"2023-02-18T00:45:54.031Z","dependency_job_id":"dd387ec2-5445-4a69-8fdb-9e2ad21d9ef5","html_url":"https://github.com/palkan/view_component-contrib","commit_stats":{"total_commits":68,"total_committers":12,"mean_commits":5.666666666666667,"dds":0.25,"last_synced_commit":"bea3236bba4dabc3dd68cb406fc9a050689bbbe2"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fview_component-contrib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fview_component-contrib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fview_component-contrib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fview_component-contrib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/palkan","download_url":"https://codeload.github.com/palkan/view_component-contrib/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254169007,"owners_count":22026207,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest","rails","view-components"],"created_at":"2024-08-06T08:02:06.532Z","updated_at":"2025-05-14T15:06:02.583Z","avatar_url":"https://github.com/palkan.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"[![Gem Version](https://badge.fury.io/rb/view_component-contrib.svg)](https://rubygems.org/gems/view_component-contrib)\n[![Build](https://github.com/palkan/view_component-contrib/workflows/Build/badge.svg)](https://github.com/palkan/view_component-contrib/actions)\n\n# View Component: extensions, examples and development tools\n\nThis repository contains various code snippets and examples related to the [ViewComponent][] library. The goal of this project is to share common patterns and practices which we found useful while working on different projects (and which haven't been or couldn't be proposed to the upstream).\n\nAll extensions and patches are packed into a `view_component-contrib` _meta-gem_. So, to use them add to your Gemfile:\n\n```ruby\ngem \"view_component-contrib\"\n```\n\n\u003ca href=\"https://evilmartians.com/\"\u003e\n\u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\" alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\u003c/a\u003e\n\n## Installation and generating generators\n\n**NOTE:** We highly recommend to walk through this document before running the generator.\n\nThe easiest way to start using `view_component-contrib` extensions and patterns is to run an interactive generator (a custom [Rails template][railsbytes-template]).\n\nAll you need to do is to run:\n\n```sh\nrails app:template LOCATION=\"https://railsbytes.com/script/zJosO5\"\n```\n\nThe command above:\n\n- Installs `view_component-contrib` gem.\n- Configure `view_component` paths.\n- Adds `ApplicationViewComponent` and `ApplicationViewComponentPreview` classes.\n- Configures testing framework (RSpec or Minitest).\n- **Adds a custom generator to create components**.\n\nThe custom generator would allow you to create all the required component files in a single command:\n\n```sh\nbundle exec rails g view_component Example\n\n# see all available options\nbundle exec rails g view_component -h\n```\n\n**Why adding a custom generator to the project instead of bundling it into the gem?** The generator could only be useful if it fits\nyour project needs. The more control you have over the generator the better. Thus, the best way is to make the generator a part of a project.\n\n\u003e [!IMPORTANT]\n\u003e If your application has the `lib/` folder in the autoload paths, make sure you ignored the generated `lib/generators` folder. In Rails 7.1+, you can do this via adding `generators` the `config.autoload_lib` call's `ignore` option. Before, you can use `Rails.autoloaders.main.ignore(...)`.\n\n## Organizing components, or sidecar pattern extended\n\nViewComponent provides different ways to organize your components: putting everyhing (Ruby files, templates, etc.) into `app/components` folder or using a _sidecar_ directory for everything but the `.rb` file itself. The first approach could easily result in a directory bloat; the second is better though there is a room for improvement: we can move `.rb` files into sidecar folders as well. Then, we can get rid of the _noisy_ `_component` suffixes. Finally, we can also put previews there (since storing them within the test folder is a little bit confusing):\n\n```txt\ncomponents/                                 components/\n  example_component/                          example/\n    example_component.html                       component.html\n  example_component.rb              →            component.rb\ntest/                                            preview.rb\n  components/                                    index.css\n    previews/                                    index.js\n      example_component_preview.rb\n```\n\nThus, everything related to a particular component (except tests, at least for now) is located within a single folder.\n\nThe two base classes are added to follow the Rails way: `ApplicationViewComponent` and `ApplicationViewComponentPreview`.\n\nWe also put the `components` folder into the `app/frontend` folder, because `app/components` is too general and could be used for other types of components, not related to the view layer.\n\nHere is an example Rails configuration:\n\n```ruby\nconfig.autoload_paths \u003c\u003c Rails.root.join(\"app\", \"frontend\", \"components\")\n```\n\n### Organizing previews\n\nFirst, we need to specify the lookup path for previews in the app's configuration:\n\n```ruby\nconfig.view_component.preview_paths \u003c\u003c Rails.root.join(\"app\", \"frontend\", \"components\")\n```\n\nBy default, ViewComponent requires preview files to have `_preview.rb` suffix, and it's not configurable (yet). To overcome this, we have to patch the `ViewComponent::Preview` class:\n\n```ruby\n# you can put this into an initializer\nActiveSupport.on_load(:view_component) do\n  ViewComponent::Preview.extend ViewComponentContrib::Preview::Sidecarable\nend\n```\n\nYou can still continue using preview clases with the `_preview.rb` suffix, they would work as before.\n\n#### Reducing previews boilerplate\n\nIn most cases, previews contain only the `default` example and a very simple template (`= render Component.new(**options)`).\nWe provide a `ViewComponentContrib::Preview` class, which helps to reduce the boilerplate by re-using templates and providing a handful of helpers.\n\nThe default template shipped with the gem is as follows:\n\n```erb\n\u003cdiv class=\"\u003c%= container_class %\u003e\"\u003e\n  \u003c%- if component -%\u003e\n    \u003c%= render component %\u003e\n  \u003c%- else -%\u003e\n    Failed to infer a component from the preview: \u003c%= error %\u003e\n  \u003c%- end -%\u003e\n\u003c/div\u003e\n```\n\nTo define your own default template:\n```ruby\nclass ApplicationViewComponentPreview \u003c ViewComponentContrib::Preview::Base\n  # ...\n  self.default_preview_template = \"path/to/your/template.html.{erb,haml,slim}\"\n  # ...\nend\n```\n\nLet's assume that you have the following `ApplicationViewComponentPreview`:\n\n```ruby\nclass ApplicationViewComponentPreview \u003c ViewComponentContrib::Preview::Base\n  # Do not show this class in the previews index\n  self.abstract_class = true\nend\n```\n\nIt allows to render a component instances within a configurable container. The component could be either created explicitly in the preview action:\n\n```ruby\nclass Banner::Preview \u003c ApplicationViewComponentPreview\n  def default\n    render_component Banner::Component.new(text: \"Welcome!\")\n  end\nend\n```\n\nOr implicitly:\n\n```ruby\nclass LikeButton::Preview \u003c ApplicationViewComponentPreview\n  def default\n    # Nothing here; the preview class would try to build a component automatically\n    # calling `LikeButton::Component.new`\n  end\nend\n```\n\nTo provide the container class, you should either specify it in the preview class itself or within a particular action by calling `#render_with`:\n\n```ruby\nclass Banner::Preview \u003c ApplicationViewComponentPreview\n  self.container_class = \"absolute w-full\"\n\n  def default\n    # This will use `absolute w-full` for the container class\n    render_component Banner::Component.new(text: \"Welcome!\")\n\n    # or even shorter\n    render_component(text: \"Welcome!\")\n\n    # you can also pass a content block\n    render_component(kind: :notice) do\n      \"Some content\"\n    end\n  end\n\n  def mobile\n    render_with(\n      component: Banner::Component.new(text: \"Welcome!\").with_variant(:mobile),\n      container_class: \"w-25\"\n    )\n  end\nend\n```\n\nIf you need more control over your template, you can add a custom `preview.html.*` template (which will be used for all examples in this preview), or even create an example-specific `previews/example.html.*` (e.g. `previews/mobile.html.erb`).\n\n## Style variants\n\nSince v0.2.0, we provide a custom extentions to manage CSS classes and their combinations—**Style Variants**. This is especially useful for project using CSS frameworks such as TailwindCSS.\n\nThe idea is to define variants schema in the component class and use it to compile the resulting list of CSS classes. (Inspired by [Tailwind Variants](https://www.tailwind-variants.org) and [CVA variants](https://cva.style/docs/getting-started/variants)).\n\nConsider an example:\n\n```ruby\nclass ButtonComponent \u003c ViewComponent::Base\n  include ViewComponentContrib::StyleVariants\n\n  style do\n    base {\n      %w[\n        font-medium bg-blue-500 text-white rounded-full\n      ]\n    }\n    variants {\n      color {\n        primary { %w[bg-blue-500 text-white] }\n        secondary { %w[bg-purple-500 text-white] }\n      }\n      size {\n        sm { \"text-sm\" }\n        md { \"text-base\" }\n        lg { \"px-4 py-3 text-lg\" }\n      }\n      disabled {\n        yes { \"opacity-75\" }\n      }\n    }\n    defaults { {size: :md, color: :primary} }\n  end\n\n  attr_reader :size, :color, :disabled\n\n  def initialize(size: nil, color: nil, disabled: false)\n    @size = size\n    @color = color\n    @disabled = disabled\n  end\nend\n```\n\nNow, in the template, you can use the `#style` method and pass the variants to it:\n\n```erb\n\u003cbutton class=\"\u003c%= style(size:, color:) %\u003e\"\u003eClick me\u003c/button\u003e\n```\n\nPassing `size: :lg` and `color: :secondary` would result in the following HTML:\n\n```html\n\u003cbutton class=\"font-medium bg-purple-500 text-white rounded-full px-4 py-3 text-lg\"\u003eClick me\u003c/button\u003e\n```\n\nThe `true` / `false` variant value would be converted into the `yes` / `no` variants:\n\n```erb\n\u003cbutton class=\"\u003c%= style(size:, color:, disabled: true) %\u003e\"\u003eClick me\u003c/button\u003e\n```\n\n**NOTE:** If you pass `nil`, the default value would be used.\n\nYou can define multiple style sets in a single component:\n\n```ruby\nclass ButtonComponent \u003c ViewComponent::Base\n  include ViewComponentContrib::StyleVariants\n\n  # default component styles\n  style do\n    # ...\n  end\n\n  style :image do\n    variants {\n      orient {\n        portrait { \"w-32 h-32\" }\n        landscape { \"w-64 h-32\" }\n      }\n    }\n  end\nend\n```\n\nAnd in the template:\n\n```erb\n\u003cdiv\u003e\n  \u003cbutton class=\"\u003c%= style(size:, theme:) %\u003e\"\u003eClick me\u003c/button\u003e\n  \u003cimg src=\"...\" class=\"\u003c%= style(:image, orient: :portrait) %\u003e\"\u003e\n\u003c/div\u003e\n```\n\nYou can also add additional classes through thr `style` method using the special `class:` variant, like so:\n\n```erb\n\u003cdiv\u003e\n  \u003cbutton class=\"\u003c%= style(size:, theme:, class: 'extra-class') %\u003e\"\u003eClick me\u003c/button\u003e\n  \u003cimg src=\"...\" class=\"\u003c%= style(:image, orient: :portrait) %\u003e\"\u003e\n\u003c/div\u003e\n```\n\nFinally, you can inject into the class list compilation process to add your own logic:\n\n```ruby\nclass ButtonComponent \u003c ViewComponent::Base\n  include ViewComponentContrib::StyleVariants\n\n  # You can provide either a proc or any other callable object\n  style_config.postprocess_with do |classes|\n    # classes is an array of CSS classes\n    # NOTE: This is an abstract TailwindMerge class, not to be confused with existing libraries\n    TailwindMerge.call(classes).join(\" \")\n  end\nend\n```\n\n### Style variants inheritance\n\nStyle variants support three inheritance strategies when extending components:\n\n1. `override` (default behavior): Completely replaces parent variants.\n2. `merge` (deep merge): Preserves all variant keys unless explicitly overwritten.\n3. `extend` (shallow merge): Preserves variants unless explicitly overwritten.\n\nConsider an example:\n\n```ruby\nclass Parent::Component \u003c ViewComponent::Base\n  include ViewComponentContrib::StyleVariants\n\n  style do\n    variants do\n      size {\n        md { \"text-md\" }\n        lg { \"text-lg\" }\n      }\n      disabled {\n        yes { \"opacity-50\" }\n      }\n    end\n  end\nend\n\n# Using override strategy (default)\nclass Child::Component \u003c Parent::Component\n  style do\n    variants do\n      size {\n        lg { \"text-larger\" }\n      }\n    end\n  end\nend\n\n# Using merge strategy\nclass Child::Component \u003c Parent::Component\n  style do\n    variants(strategy: :merge) do\n      size {\n        lg { \"text-larger\" }\n      }\n    end\n  end\nend\n\n# Using extend strategy\nclass Child::Component \u003c Parent::Component\n  style do\n    variants(strategy: :extend) do\n      size {\n        lg { \"text-larger\" }\n      }\n    end\n  end\nend\n```\n\nIn this example, the `override` strategy will only keep the `size.lg` variant, dropping all others. The `merge` strategy preserves all variants and their keys, only replacing the `size.lg` value. The `extend` strategy keeps all variants but replaces all keys of the overwritten `size` variant.\n\n### Dependent (or compound) styles\n\nSometimes it might be necessary to define complex styling rules, e.g., when a combination of variants requires adding additional styles. That's where usage of Ruby blocks for configuration becomes useful. For example:\n\n```ruby\nstyle do\n  variants {\n    size {\n      sm { \"text-sm\" }\n      md { \"text-base\" }\n      lg { \"px-4 py-3 text-lg\" }\n    }\n    theme {\n      primary do |size:, **|\n        %w[bg-blue-500 text-white].tap do\n          _1 \u003c\u003c \"uppercase\" if size == :lg\n        end\n      end\n      secondary { %w[bg-purple-500 text-white] }\n    }\n  }\nend\n```\n\nThe specified variants are passed as block arguments, so you can implement dynamic styling.\n\nIf you prefer declarative approach, you can use the special `compound` directive. The previous example could be rewritten as follows:\n\n```ruby\nstyle do\n  variants {\n    size {\n      sm { \"text-sm\" }\n      md { \"text-base\" }\n      lg { \"px-4 py-3 text-lg\" }\n    }\n    theme {\n      primary { %w[bg-blue-500 text-white] }\n      secondary { %w[bg-purple-500 text-white] }\n    }\n  }\n\n  compound(size: :lg, theme: :primary) { %w[uppercase] }\nend\n```\n\n### Using with TailwindCSS LSP\n\nTo make completions (and other LSP features) work with our DSL, try the following configuration:\n\n```json\n\"tailwindCSS.includeLanguages\": {\n  \"erb\": \"html\",\n  \"ruby\": \"html\"\n},\n\"tailwindCSS.experimental.classRegex\": [\n  \"%w\\\\[([^\\\\]]*)\\\\]\"\n]\n```\n\n**NOTE:** It will only work with `%w[ ... ]` word arrays, but you can adjust it to your needs.\n\n## Organizing assets (JS, CSS)\n\n**NOTE**: This section assumes the usage of Vite or Webpack. See [this discussion](https://github.com/palkan/view_component-contrib/discussions/14) for other options.\n\nWe store JS and CSS files in the same sidecar folder:\n\n```txt\ncomponents/\n  example/\n    component.html\n    component.rb\n    index.css\n    index.js\n```\n\nThe `index.js` is the controller's entrypoint; it imports the CSS file and may contain some JS code:\n\n```js\nimport \"./index.css\"\n```\n\nIn the root of the `components` folder we have the `index.js` file, which loads all the components:\n\n- With Vite:\n\n```js\n// With Vite\nimport.meta.glob(\"./**/index.js\").forEach((path) =\u003e {\n  const mod = await import(path);\n  mod.default();\n});\n```\n\n- With Webpack:\n\n```js\n// components/index.js\nconst context = require.context(\".\", true, /index.js$/)\ncontext.keys().forEach(context);\n```\n\n### Using with StimulusJS\n\nYou can define Stimulus controllers right in the component folder in the `controller.js` file:\n\n```js\n// We reserve Controller for the export name\nimport { Controller as BaseController } from \"@hotwired/stimulus\";\n\nexport class Controller extends BaseController {\n  connect() {\n    // ...\n  }\n}\n```\n\nThen, in your Stimulus entrypoint, you can load and register your component controllers as follows:\n\n- With Vite:\n\n```js\nimport { Application } from \"@hotwired/stimulus\";\n\nconst application = Application.start();\n\n// Configure Stimulus development experience\napplication.debug = false;\nwindow.Stimulus = application;\n\n// Generic controllers\nconst genericControllers = import.meta.globEager(\n  \"../controllers/**/*_controller.js\"\n);\n\nfor (let path in genericControllers) {\n  let module = genericControllers[path];\n  let name = path\n    .match(/controllers\\/(.+)_controller\\.js$/)[1]\n    .replaceAll(\"/\", \"-\")\n    .replaceAll(\"_\", \"-\");\n\n  application.register(name, module.default);\n}\n\n// Controllers from components\nconst controllers = import.meta.globEager(\n  \"./../../app/frontend/components/**/controller.js\"\n);\n\nfor (let path in controllers) {\n  let module = controllers[path];\n  let name = path\n    .match(/app\\/frontend\\/components\\/(.+)\\/controller\\.js$/)[1]\n    .replaceAll(\"/\", \"-\")\n    .replaceAll(\"_\", \"-\");\n  application.register(name, module.default);\n}\n\nexport default application;\n```\n\n- With Webpack:\n\n```js\nimport { Application } from \"stimulus\";\nexport const application = Application.start();\n\n// ... other controllers\n\nconst context = require.context(\"./../../app/frontend/components/\", true, /controllers.js$/)\ncontext.keys().forEach((path) =\u003e {\n  const mod = context(path);\n\n  // Check whether a module has the Controller export defined\n  if (!mod.Controller) return;\n\n  // Convert path into a controller identifier:\n  //   example/index.js -\u003e example\n  //   nav/user_info/index.js -\u003e nav--user-info\n  const identifier = path\n    .match(/app\\/frontend\\/components\\/(.+)\\/controller\\.js$/)[1]\n    .replaceAll(\"/\", \"-\")\n    .replaceAll(\"_\", \"-\");\n\n  application.register(identifier, mod.Controller);\n});\n```\n\nWe also can add a helper to our base ViewComponent class to generate the controller identifier following the convention above:\n\n```ruby\nclass ApplicationViewComponent\n  private\n\n  def identifier\n    @identifier ||= self.class.name.sub(\"::Component\", \"\").underscore.split(\"/\").join(\"--\")\n  end\n\n  alias_method :controller_name, :identifier\nend\n```\n\nAnd now in your template:\n\n```erb\n\u003c!-- component.html --\u003e\n\u003cdiv data-controller=\"\u003c%= controller_name %\u003e\"\u003e\n\u003c/div\u003e\n```\n\n### Isolating CSS with postcss-modules\n\nOur JS code is isolated by design but our CSS is still global. Hence we should care about naming, use some convention (such as BEM) or whatever.\n\nAlternatively, we can leverage the power of modern frontend technologies such as [CSS modules][] via [postcss-modules][] plugin. It allows you to use _local_ class names in your component, and takes care of generating unique names in build time. We can configure PostCSS Modules to follow our naming convention, so, we can generate the same unique class names in both JS and Ruby.\n\nFirst, install the `postcss-modules` plugin (`yarn add postcss-modules`).\n\nThen, add the following to your `postcss.config.js`:\n\n```js\nmodule.exports = {\n  plugins: {\n    'postcss-modules': {\n      generateScopedName: (name, filename, _css) =\u003e {\n        const matches = filename.match(/\\/app\\/frontend\\/components\\/?(.*)\\/index.css$/);\n        // Do not transform CSS files from outside of the components folder\n        if (!matches) return name;\n\n        // identifier here is the same identifier we used for Stimulus controller (see above)\n        const identifier = matches[1].replace(\"/\", \"--\");\n\n        // We also add the `c-` prefix to all components classes\n        return `c-${identifier}-${name}`;\n      },\n      // Do not generate *.css.json files (we don't use them)\n      getJSON: () =\u003e {}\n    },\n    /// other plugins\n  },\n}\n```\n\nFinally, let's add a helper to our view components:\n\n```ruby\nclass ApplicationViewComponent\n  private\n\n  # the same as above\n  def identifier\n    @identifier ||= self.class.name.sub(\"::Component\", \"\").underscore.split(\"/\").join(\"--\")\n  end\n\n  # We also add an ability to build a class from a different component\n  def class_for(name, from: identifier)\n    \"c-#{from}-#{name}\"\n  end\nend\n```\n\nAnd now in your template:\n\n```erb\n\u003c!-- example/component.html --\u003e\n\u003cdiv class=\"\u003c%= class_for(\"container\") %\u003e\"\u003e\n  \u003cp class=\"\u003c%= class_for(\"body\") %\u003e\"\u003e\u003c%= text %\u003e\u003c/p\u003e\n\u003c/div\u003e\n```\n\nAssuming that you have the following `index.css`:\n\n```css\n.container {\n  padding: 10px;\n  background: white;\n  border: 1px solid #333;\n}\n\n.body {\n  margin-top: 20px;\n  font-size: 24px;\n}\n```\n\nThe final HTML output would be:\n\n```html\n\u003cdiv class=\"c-example-container\"\u003e\n  \u003cp class=\"c-example-body\"\u003eSome text\u003c/p\u003e\n\u003c/div\u003e\n```\n\n## I18n integration (alternative)\n\nViewComponent recently added (experimental) [I18n support](https://github.com/github/view_component/pull/660), which allows you to have **isolated** localization files for each component. Isolation rocks, but managing dozens of YML files spread accross the project could be tricky, especially, if you rely on some external localization tool which creates these YMLs for you.\n\nWe provide an alternative (and more _classic_) way of dealing with translations—**namespacing**. Following the convention over configuration,\nput translations under `\u003clocale\u003e.view_components.\u003ccomponent_scope\u003e` key, for example:\n\n```yml\nen:\n  view_components:\n    login_form:\n      submit: \"Log in\"\n    nav:\n      user_info:\n        login: \"Log in\"\n        logout: \"Log out\"\n```\n\nAnd then in your components:\n\n```erb\n\u003c!-- login_form/component.html.erb --\u003e\n\u003cbutton type=\"submit\"\u003e\u003c%= t(\".submit\") %\u003e\u003c/button\u003e\n\n\u003c!-- nav/user_info/component.html.erb --\u003e\n\u003ca href=\"/logout\"\u003e\u003c%= t(\".logout\") %\u003e\u003c/a\u003e\n```\n\nIf you're using `ViewComponentContrib::Base`, you already have translation support included.\nOthwerwise you must include the module yourself:\n\n```ruby\nclass ApplicationViewComponent \u003c ViewComponent::Base\n  include ViewComponentContrib::TranslationHelper\nend\n```\n\nYou can override the default namespace (`view_components`) and a particular component _scope_:\n\n```ruby\nclass ApplicationViewComponent \u003c ViewComponentContrib::Base\n  self.i18n_namespace = \"my_components\"\nend\n\nclass SomeButton::Component \u003c ApplicationViewComponent\n  self.i18n_scope = %w[legacy button]\nend\n```\n\n## Hanging `#initialize` out to Dry\n\nOne way to improve development experience with ViewComponent is to move from imperative `#initialize` to something declarative.\nOur choice is [dry-initializer][].\n\nAssuming that we have the following component:\n\n```ruby\nclass FlashAlert::Component \u003c ApplicationViewComponent\n  attr_reader :type, :duration, :body\n\n  def initialize(body:, type: \"success\", duration: 3000)\n    @body = body\n    @type = type\n    @duration = duration\n  end\nend\n```\n\nLet's add `dry-initializer` to our base class:\n\n```ruby\nclass ApplicationViewComponent\n  extend Dry::Initializer\nend\n```\n\nAnd then refactor our FlashAlert component:\n\n```ruby\nclass FlashAlert::Component \u003c ApplicationViewComponent\n  option :type, default: proc { \"success\" }\n  option :duration, default: proc { 3000 }\n  option :body\nend\n```\n\n## Supporting `.with_collection`\n\nThe `.with_collection` method from ViewComponent expects a component class to have the \"Component\" suffix to correctly infer the parameter name. Since we're using a different naming convention, we need to specify the collection parameter name explicitly. For example:\n\n```ruby\nclass PostCard::Component \u003c ApplicationViewComponent\n  with_collection_parameter :post\n\n  option :post\nend\n```\n\nYou can add this to following line to your component generator (unless it's already added): `with_collection_parameter :\u003c%= singular_name %\u003e` to always explicitly provide the collection parameter name.\n\n## Wrapped components\n\nSometimes we need to wrap a component into a custom HTML container (for positioning or whatever). By default, such wrapping doesn't play well with the `#render?` method because if we don't need a component, we don't need a wrapper.\n\nTo solve this problem, we introduce a special `ViewComponentContrib::WrapperComponent` class: it takes any component as the only argument and accepts a block during rendering to define a wrapping HTML. And it renders only if the _inner component_'s `#render?` method returns true.\n\n```erb\n\u003c%= render ViewComponentContrib::WrappedComponent.new(Example::Component.new) do |wrapper| %\u003e\n  \u003cdiv class=\"col-md-auto mb-4\"\u003e\n    \u003c%= wrapper.component %\u003e\n  \u003c/div\u003e\n\u003c%- end -%\u003e\n```\n\nYou can add a `#wrapped` method to your base class to simplify the code above:\n\n```ruby\nclass ApplicationViewComponent \u003c ViewComponent::Base\n  # adds #wrapped method\n  # NOTE: Already included into ViewComponentContrib::Base\n  include ViewComponentContrib::WrappedHelper\nend\n```\n\nAnd the template looks like this now:\n\n```erb\n\u003c%= render Example::Component.new.wrapped do |wrapper| %\u003e\n  \u003cdiv class=\"col-md-auto mb-4\"\u003e\n    \u003c%= wrapper.component %\u003e\n  \u003c/div\u003e\n\u003c%- end -%\u003e\n```\n\nYou can use the `#wrapped` method on any component inherited from `ApplicationViewComponent` to wrap it automatically:\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n[ViewComponent]: https://github.com/github/view_component\n[postcss-modules]: https://github.com/madyankin/postcss-modules\n[CSS modules]: https://github.com/css-modules/css-modules\n[dry-initializer]: https://dry-rb.org/gems/dry-initializer\n[railsbytes-template]: https://railsbytes.com/templates/zJosO5\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fview_component-contrib","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpalkan%2Fview_component-contrib","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fview_component-contrib/lists"}