Custom Ingredients
Alchemy provides many built-in ingredient types, but you can create your own to store custom data or associate ingredients with your existing models.
Each custom ingredient consists of a model class, a view component for the frontend, and an editor component for the admin interface. Alchemy uses ViewComponent for both.
TIP
The alchemy-solidus extension is a good real-world example. It provides SpreeProduct, SpreeTaxon and SpreeVariant ingredient types that associate Alchemy elements with Solidus e-commerce models.
Using the Generator
The ingredient generator creates the model and both components for you.
bin/rails g alchemy:ingredient MyIngredientThis creates three files:
app/models/alchemy/ingredients/my_ingredient.rb- the modelapp/components/alchemy/ingredients/my_ingredient_view.rb- the frontend viewapp/components/alchemy/ingredients/my_ingredient_editor.rb- the admin editor
TIP
Ingredients live under the Alchemy::Ingredients namespace. The type name in your elements.yml is the PascalCase class name, e.g. MyIngredient.
The Model
The generated model extends Alchemy::Ingredient. The main content is stored in the value column (a text column).
# app/models/alchemy/ingredients/my_ingredient.rb
module Alchemy
module Ingredients
class MyIngredient < Alchemy::Ingredient
# Set additional attributes that get stored in the `data` JSON column
# store_accessor :data, :some, :attribute
# Set a related_object alias for convenience
# related_object_alias :some_association_name, class_name: "Some::Klass"
end
end
endIf you need to store additional values beyond value, add them as attributes to the data column using store_accessor. Rails will create accessor methods for you.
Type Casting the Value
Since value is stored as text, you may want to cast it to a different type. Override the value method and use ActiveRecord type casting.
# Cast to boolean (like the built-in Boolean ingredient)
def value
ActiveRecord::Type::Boolean.new.cast(self[:value])
end
# Cast to datetime (like the built-in Datetime ingredient)
def value
ActiveRecord::Type::DateTime.new.cast(self[:value])
end
# Cast to integer
def value
ActiveRecord::Type::Integer.new.cast(self[:value])
endAllowing Settings
Use allow_settings to define which settings from elements.yml are available to your ingredient. Only safelisted settings are accessible via the settings hash.
class MyIngredient < Alchemy::Ingredient
allow_settings %i[format display_mode]
end# config/alchemy/elements.yml
- name: my_element
ingredients:
- role: custom
type: MyIngredient
settings:
format: short
display_mode: inlineCustomizing the Preview Text
The preview_text method controls what is shown in the element's title bar in the admin. By default it shows the first 30 characters of value. Override it to show something more meaningful.
class Person < Alchemy::Ingredient
related_object_alias :person, class_name: "My::Person"
def preview_text(maxlength = 30)
return unless person
person.name[0..maxlength - 1]
end
endThe View Component
The view component controls how the ingredient is rendered on your website. It extends Alchemy::Ingredients::BaseView.
# app/components/alchemy/ingredients/my_ingredient_view.rb
module Alchemy
module Ingredients
class MyIngredientView < BaseView
def call
content_tag(:div, ingredient.value)
end
end
end
endThe ingredient accessor gives you access to the ingredient record, including its value, settings, and any data attributes or associations. Override call to return the HTML you want to render.
Override render? to control when the component renders. By default it renders when the ingredient has a value.
class PersonView < BaseView
delegate :person, to: :ingredient
def call
content_tag(:span, person.name)
end
def render?
!!person
end
endThe Editor Component
The editor component controls the form fields in the admin interface. It extends Alchemy::Ingredients::BaseEditor.
# app/components/alchemy/ingredients/my_ingredient_editor.rb
module Alchemy
module Ingredients
class MyIngredientEditor < BaseEditor
# The default editor renders a text field for the `value` column.
# Override `input_field` to customize the form input.
#
# def input_field
# text_field_tag(form_field_name, value)
# end
end
end
endThe base editor provides helpers for building form fields:
form_field_name(column)- generates the correct form field name for a column (defaults to"value")form_field_id(column)- generates the correct form field ID for a columnvalue- the current ingredient valuesettings- the ingredient's settings from the element definition
Override the input_field method to customize the editor UI. For example, to render a select box:
class MyIngredientEditor < BaseEditor
def input_field
select_tag(
form_field_name,
options_for_select(available_options, value),
class: "full_width"
)
end
private
def available_options
settings[:options] || []
end
endWARNING
Avoid loading large collections into the editor. If your input_field renders a select_tag with all records from a model (e.g. all products or all people), every element editor will trigger a database query for the full set. This causes N+1 queries and slow admin pages.
Instead, use a remote select that fetches results on demand via an API endpoint. Alchemy ships a RemoteSelect JavaScript base class (alchemy_admin/components/remote_select) that provides paginated, searchable selects using Custom Elements. Your custom element extends RemoteSelect, implements _searchQuery() and _parseResponse(), and your editor component renders the custom element tag instead of a select_tag.
The alchemy-solidus extension demonstrates this pattern. It defines <alchemy-product-select>, <alchemy-variant-select> and <alchemy-taxon-select> custom elements that query the Solidus API with pagination and search, avoiding loading thousands of records into memory.
Associating with Models
You can associate any ActiveRecord model with an ingredient using related_object_alias. This stores the foreign key in the related_object_id column and sets up a belongs_to association.
# app/models/alchemy/ingredients/person.rb
module Alchemy
module Ingredients
class Person < Alchemy::Ingredient
related_object_alias :person, class_name: "My::Person"
def preview_text(maxlength = 30)
return unless person
person.name[0..maxlength - 1]
end
end
end
endAlchemy handles the rest. You can access the associated model in your view component.
# app/components/alchemy/ingredients/person_view.rb
module Alchemy
module Ingredients
class PersonView < BaseView
delegate :person, to: :ingredient
def call
content_tag(:span, person.name)
end
def render?
!!person
end
end
end
endUsing Your Ingredient
Add your custom ingredient to any element definition in elements.yml.
- name: team_member
ingredients:
- role: person
type: PersonSee the Elements guide for more on defining elements.