|
| 1 | +# Customizing view components |
| 2 | + |
| 3 | +Solidus Admin uses [view components](https://viewcomponent.org/) to render the views. Components are |
| 4 | +a pattern for breaking up the view layer into small, reusable pieces, easy to |
| 5 | +reason about and test. |
| 6 | + |
| 7 | +All the components Solidus Admin uses are located in the [`app/components`](../app/components) folder of the |
| 8 | +`solidus_admin` gem. As you can see, they are organized in a particular folder structure: |
| 9 | + |
| 10 | +- All of them are under the `SolidusAdmin` namespace. |
| 11 | +- They are grouped in sidecar directories, where the main component file and |
| 12 | + all its related files (assets, i18n files, etc.) live together. |
| 13 | + |
| 14 | +For instance, the component for the main navigation is located in |
| 15 | +[`app/components/solidus_admin/main_nav/component.rb`](../app/components/solidus_admin/main_nav/component.rb). |
| 16 | + |
| 17 | +Solidus Admin components are designed to be easily customizable by the host |
| 18 | +application. Because of that, if you look at how they are designed, you'll find |
| 19 | +a series of patterns are followed consistently: |
| 20 | + |
| 21 | +- Components are always resolved from a global registry in |
| 22 | + `SolidusAdmin::Container` instead of being referenced by their constant. For |
| 23 | + example, we call `component('main_nav')` instead of referencing |
| 24 | + `SolidusAdmin::MainNav::Component` directly. As you'll see later, this makes |
| 25 | + it easy to replace or tweak a component. |
| 26 | +- When a component needs to render another component, it gets it as an argument |
| 27 | + for its `#initialize` method and assigns it to an instance variable ending in |
| 28 | + `_component`. When rendering from the template, it calls the `render` method |
| 29 | + on the instance variable instead of resolving the component in-line. Because |
| 30 | + of that, switching a nested component only requires modifying its |
| 31 | + initializer. |
| 32 | +- Those components given on initialization are always defaulted to its expected |
| 33 | + value resolved from the container. That means that callers don't need to |
| 34 | + provide them explicitly, and that they can be replaced by custom components. |
| 35 | +- In any case, Solidus Admin components initializers only take keyword |
| 36 | + arguments. |
| 37 | + |
| 38 | +A picture is worth a thousand words, so let's depict how this works with an |
| 39 | +example: |
| 40 | + |
| 41 | +```ruby |
| 42 | +# app/components/solidus_admin/foo/component.rb |
| 43 | +class SolidusAdmin::Foo::Component < SolidusAdmin::BaseComponent |
| 44 | + def initialize(bar_component: component('bar')) |
| 45 | + @bar_component = bar_component |
| 46 | + end |
| 47 | + |
| 48 | + erb_template <<~ERB |
| 49 | + <div> |
| 50 | + <%= render @bar_component.new %> |
| 51 | + </div> |
| 52 | + ERB |
| 53 | +end |
| 54 | +# render component('foo').new |
| 55 | +``` |
| 56 | + |
| 57 | +## Customizing components |
| 58 | + |
| 59 | +Some of the customizations detailed below require you to match Solidus Admin's |
| 60 | +component paths in your application. For instance, if we talk about the |
| 61 | +component in `solidus_admin` gem's |
| 62 | +`app/components/solidus_admin/main_nav/component.rb`, the matching path in your |
| 63 | +application would be |
| 64 | +`app/components/my_application/solidus_admin/main_nav/component.rb`, where |
| 65 | +`my_application` is the underscored name of your application (you can get it by |
| 66 | +running `Rails.application.class.module_parent_name`). |
| 67 | + |
| 68 | +### Replacing a component's template |
| 69 | + |
| 70 | +> ⓘ This is not possible yet because of |
| 71 | +> https://github.com/ViewComponent/view_component/issues/411, but it would |
| 72 | +> probably be worth trying to find a way to make it work. Otherwise, we can |
| 73 | +> describe how to override the `erb_template` call. |
| 74 | +
|
| 75 | +In the most typical case, you'll only need to replace the template used by a |
| 76 | +component. You can do that by creating a new template with a maching path in |
| 77 | +your application. For example, to replace the main nav template, you can create: |
| 78 | + |
| 79 | +```erb |
| 80 | +<%# app/components/my_application/solidus_admin/main_nav/template.html.erb %> |
| 81 | +<nav class="my_own_classes"> |
| 82 | + <%= |
| 83 | + render main_nav_item_component.with_collection( |
| 84 | + sorted_items |
| 85 | + ) |
| 86 | + %> |
| 87 | +</nav> |
| 88 | +``` |
| 89 | + |
| 90 | +### Prepending or appending to a component's template |
| 91 | + |
| 92 | +In some situations, you might only need to add some markup before or after a |
| 93 | +component. You can easily do that by rendering the Solidus Admin component and |
| 94 | +adding your markup before or after it. |
| 95 | + |
| 96 | +```erb |
| 97 | +<%# app/components/my_application/solidus_admin/main_nav/template.html.erb %> |
| 98 | +<h1>MY STORE ADMINISTRATION</h1> |
| 99 | +<%= render SolidusAdmin::MainNav::Component.new %> |
| 100 | +``` |
| 101 | + |
| 102 | +### Replacing a component |
| 103 | + |
| 104 | +You can replace a component by creating a new one with a matching path in your |
| 105 | +application. |
| 106 | + |
| 107 | +There are two considerations to keep in mind: |
| 108 | + |
| 109 | +- Be aware that other components might be using the component you're replacing. |
| 110 | + They should only be using its `#initialize` method, so make sure to keep |
| 111 | + compatibility with it when they're called. |
| 112 | +- Solidus Admin's components always inherit from |
| 113 | + [SolidusAdmin::BaseComponent](../app/components/solidus_admin/base_component.rb). |
| 114 | + You can consider doing the same if you need to use one of its helpers. |
| 115 | + |
| 116 | +For example, the following replaces the main nav component: |
| 117 | + |
| 118 | +```ruby |
| 119 | +# app/components/my_application/solidus_admin/main_nav/component.rb |
| 120 | +class MyApplication::SolidusAdmin::MainNav::Component < SolidusAdmin::BaseComponent |
| 121 | + # do your thing |
| 122 | +end |
| 123 | +``` |
| 124 | + |
| 125 | +If you need more control, you can explicitly register your component in the |
| 126 | +Solidus Admin container instead of using an implicit path: |
| 127 | + |
| 128 | +> ⓘ Right now, that will raise an error when the application is reloaded. We |
| 129 | +> need to fix it. |
| 130 | +
|
| 131 | +```ruby |
| 132 | +# config/initalizers/solidus_admin.rb |
| 133 | +Rails.application.config.to_prepare do |
| 134 | + SolidusAdmin::Container['components.main_nav.component'] = OtherNamespace::NavComponent |
| 135 | +end |
| 136 | +``` |
| 137 | + |
| 138 | +### Tweaking a component |
| 139 | + |
| 140 | +If you only need to tweak a component, you can always inherit from it in a |
| 141 | +matching path from within your application (or manually register in the |
| 142 | +container) and override the methods you need to change: |
| 143 | + |
| 144 | +```ruby |
| 145 | +# app/components/my_application/solidus_admin/main_nav/component.rb |
| 146 | +class MyApplication::SolidusAdmin::MainNav::Component < ::SolidusAdmin::MainNav::Component |
| 147 | + def sorted_items |
| 148 | + super.reverse |
| 149 | + end |
| 150 | +end |
| 151 | +``` |
| 152 | + |
| 153 | +Be aware that this approach comes with an important trade-off: You'll need to |
| 154 | +keep your component in sync with the original one as it changes on future updates. |
| 155 | +For instance, in the example above, the component is overriding a private |
| 156 | +method, so there's no guarantee that it will continue to exist in the future |
| 157 | +without being deprecated, as we only guarantee public API stability. |
| 158 | + |
| 159 | +### Replacing or tweaking a nested component |
| 160 | + |
| 161 | +If you need to customize a nested component that is used in a single place, you |
| 162 | +can do that by following our previous instructions on [replacing a |
| 163 | +component](#replacing-a-component) or [tweaking a component](#tweaking-a-component). |
| 164 | + |
| 165 | +However, if the component you want to customize is used in multiple places, you |
| 166 | +can replace the default component called from the parent one. |
| 167 | + |
| 168 | +For the sake of example, let's say that the main navigation item component is used in |
| 169 | +multiple places, but you only need to change the one rendered from the main |
| 170 | +navigation. To begin with, you'd need to manually register in the container the |
| 171 | +component you want to use: |
| 172 | + |
| 173 | +```ruby |
| 174 | +Rails.application.config.to_prepare do |
| 175 | + SolidusAdmin::Container.register('components.main_nav_item_foo.component', MyApplication::SolidusAdmin::MainNavItemFoo::Component) |
| 176 | +end |
| 177 | +``` |
| 178 | + |
| 179 | +Then, you can change the default value of the `main_nav_item_component` |
| 180 | +argument from the top-level nav component: |
| 181 | + |
| 182 | +```ruby |
| 183 | +# app/components/my_application/solidus_admin/main_nav/component.rb |
| 184 | +class MyApplication::SolidusAdmin::MainNav::Component < ::SolidusAdmin::MainNav::Component |
| 185 | + def initialize(main_nav_item_component: component('main_nav_item_foo'), **kwargs) |
| 186 | + super |
| 187 | + end |
| 188 | +end |
| 189 | +``` |
0 commit comments