Components are the main building blocks of the admin interface. They are implemented as ViewComponents and are rendered directly by controllers.
The following documentation assumes familiariry with ViewComponents. If you are not please refer to the ViewComponent documentation.
There are two types of components:
- UI components are the building blocks of the interface. Tipically, they are small components that are used to build more complex components and are generic enough to be reused in various contexts. UI components are located under the
app/components/solidus_admin/ui
folder. - Page components are the primary components rendered by the controllers. Generally, they are full-page components rendered directly by the controllers. They are located under the
app/components/solidus_admin
directory following the naming convention of the controller and action names they are used in. For exampleapp/components/solidus_admin/orders/index/component.rb
is the component that is rendered by theSolidusAdmin::OrdersController#index
action.
Components can be generated using the solidus_admin:component
generator combined with the bin/rails
command from the Solidus repository.
$ bin/rails admin g solidus_admin:component foo
create app/components/solidus_admin/foo/component.rb
create app/components/solidus_admin/foo/component.html.erb
create app/components/solidus_admin/foo/component.yml
create app/components/solidus_admin/foo/component.js
Using bin/rails admin
will run the generator from the solidus_admin
engine, instead of the sandbox application.
For UI components in particular, it's preferable to accept only simple Ruby values in the initializer and use alternative constructors to accept more complex objects. This makes components easier to use and test. For example:
# bad
class SolidusAdmin::UI::OrderStatus::Component < ViewComponent::Base
def initialize(order:)
@order = order
end
end
# good
class SolidusAdmin::UI::OrderStatus::Component < ViewComponent::Base
def initialize(status:)
@status = status
end
def self.for_order(order)
new(status: order.status)
end
end
For style variations within the component we use the term scheme
rather than variant
to avoid confusion with product variants.
For size variations we use the term size
with single letter values, e.g. s
, m
, l
, xl
, xxl
.
For text content we use the term text
rather than name
to avoid confusion with the name
attribute of the HTML tag.
Components are registered in the SolidusAdmin::Config.components
registry. This allows replacing components for customization purposes and components deprecation between versions.
To retrieve component classes from the registry, use the component
helper within controllers and components that inherit from SolidusAdmin::BaseComponent
or include SolidusAdmin::ComponentHelper
. For example, component('ui/button')
will fetch SolidusAdmin::UI::Button::Component
.
Generally new components are built for a specific controller action and are used only within that action. In such cases, it's better to use a Page component and define it under the namespace of the action, e.g. app/components/solidus_admin/orders/index/payment_status/component.rb
.
If a component is used by multiple actions of the same controller it can be moved to the controller namespace, e.g. app/components/solidus_admin/orders/payment_status/component.rb
.
When a component is used by multiple controllers, you can either duplicate it in multiple places or move it to the ui
namespace.
Although it may seem counterintuitive, duplicating the component can often be beneficial. This allows you to modify the component in one place without affecting other components that might be using it. Over time, the two copies may share enough generic code that it can be extracted into a UI component.
UI components should be very generic and reusable. However, they should not try to anticipate all possible use cases. Instead, they should be extracted from existing components that are already used in multiple places. This has proven to be the most effective way to build UI components. We've found that trying to anticipate theoretical use cases often leads to over-engineered code that eventually needs to be adapted to the actual use cases or is never used at all.
The project uses a naming convention for components that slightly deviates from ViewComponent defaults. This is done to simplify renaming components and moving them around.
All files related to a component have a base name of component
, each with its own extension. These files are placed in a folder named after the component class they define.
E.g. app/components/solidus_admin/orders/index/payment_status/component.rb
defines the SolidusAdmin::Orders::Index::PaymentStatus::Component
class.
With this approach, renaming a component is as simple as renaming the folder and the class name, without the need to change the names of all the files.
Components can define their own translations in the component.yml
file and they're expected to be self contained. This means that translations defined in solidus_core
should not be used in components.
Please refer to the ViewComponent documentation for more information.
For UI components we leverage ViewComponent previews combined with Lookbook to provide a live preview of the component. This approach is highly beneficial for understanding the component's appearance and how it can be modified using different arguments.
Creating previews for page components can be challenging and prone to errors, as they often require a more complex context for rendering. Therefore, we typically don't use previews for page components, except for the most basic ones. However, if a component has a wide range of arguments and we want to cover all combinations, we might create a preview for it.
In order to inspect previews it's enough to visit /lookbook
in the browser while the server is running.
Testing methods for components vary depending on whether they are UI or Page components. UI components are tested in isolation, while Page components, which often require a more complex context, are tested through feature specs.
For UI components, we use previews to achieve maximum coverage. This approach is sufficient for most basic components, but more complex components may require additional specs. This method has proven to minimize maintenance and code churn in the spec code, and it avoids repeating the code needed to render the component with different arguments.
Page components are tested in the context of the controller action they are used in. For example, admin/spec/features/orders_spec.rb
covers interactions with the order listing and indirectly tests the SolidusAdmin::Orders::Index::Component
class, among others.
We've found this to be the most effective way to test page components, as recreating the context needed for them in isolation can be difficult and prone to errors.
However, this is not a hard rule. If a Page component needs to be tested in isolation, or if a UI component requires a more complex context, you can always write additional specs.