Decorator
Apart from wrapping a record object, the decorator is used in order to hold view metadata for index, show, and form (new/edit) pages.
First of all, it’s always recommended to create a base decorator class Admin::ApplicationDecorator as below, so that devs can have better control of developing global changes/functions:
# app/decorators/admin/application_decorator.rb
class Admin::ApplicationDecorator < Wallaby::ResourceDecorator
end
This base decorator can be generated by Wallaby generator as well:
rails g wallaby:install
see Mapping - resource_decorator if
Admin::ApplicationDecoratoris taken for other purpose.
Starting with:
Configuration can be set for:
- .base_class! - flagging as base class.
- .model_class - specifying the model class.
Accessing helper methods:
Configuring the metadata for fields:
- index_fields - metadata for the index page.
- :sort_field_name - specifying the field name to sort.
- :sort_disabled - enable/disable sorting of a field.
- show_fields - metadata for the show page.
- form_fields - metadata for the form (new/edit) page.
Common options for metadata:
- :type - mandatory option to associate type partial with the field.
- :label - customizing text for a field
Configuring the fields to display:
- index_field_name - fields to display for the index page.
- show_field_name - fields to display for the show page.
- form_field_name - fields to display for the form (new/edit) page.
Defining filters to allow access to fixed queries:
Other common options for a filter:
- :scope - customizing the query
- :label - customizing text for a field
- :default - specifying the default query used on the index page.
Misc customization:
- to_label - customizing title of a resource.
- primary_key - specifying the field to be the primary key to use in query.
Read more for typical pre-generated metadata values for associations of ActiveRecord:
Declaration
Read more at Decorator Naming Convention
Let’s see how a decorator can be created so that Wallaby knows its existence.
Similar to the way in Rails, create a custom decorator for model Product inheriting from Admin::ApplicationDecorator (the base decorator mentioned above) as below:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
end
If ProductDecorator is taken, it is still possible to use another name (e.g. Admin::ProductDecorator). However, the attribute model_class must be specified. See .model_class for examples.
.base_class!
All decorators will be preloaded and processed by Wallaby in order to build up the mapping between decorators and models. If the decorator is considered not to be processed, it can be flagged by using base_class!:
# app/decorators/admin/special_decorator.rb
class Admin::SpecialDecorator < Admin::ApplicationDecorator
base_class!
end
.model_class
According to Wallaby’s Decorator Naming Convention, if a custom decorator cannot reflect the association with the correct model. For example, as Admin::ProductDecorator to Product, it is required to specify the model class in the decorator as below:
# app/decorators/admin/product_decorator.rb
class Admin::ProductDecorator < Admin::ApplicationDecorator
self.model_class = Product
end
Helper Methods
The followings are the helper methods that are available in decorator instance:
#resource
Decorator wraps the resource object. By default, it’s possible to access resource object’s public methods directly:
# app/models/product.rb
class Product < ApplicationRecord
def amount
rrp * discount
end
def currency
currency_of country
end
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def amount_in_currency
"#{ currency } #{ amount }"
end
end
In the above example, currency and amount are Product’s instance methods, and they can be accessed directly in ProductDecorator.
However, it’s still possible to access to the resource object on demand:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def amount_in_currency
"#{ resource.currency } #{ resource.amount }"
end
end
#h
It’s possible to access all Rails and Wallaby helpers via h, for example:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def amount_in_currency
h.number_to_currency resource.amount
end
end
Metadata
Decorator uses pre-generated class attributes to store the metadata to be used on the index/show/form page.
NOTE: Decorator works the same for different ORMs, the only difference is the metadata that Wallaby pre-generates for different ORMs.
index_fields
index_fields is used for storing metadata of different (origin/custom) fields for index page. For example, for model Product:
# app/models/product.rb
class Product < ApplicationRecord
has_many :order_items, class_name: Order::Item.name
has_many :orders, through: :order_items
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
end
# NOTE: Wallaby has already generated a set of metadata using the information
# from model `Product`.
ProductDecorator.index_fields
# It looks like =>
# {
# 'id' => { 'type' => 'integer', 'label' => 'Id' },
# 'description' => { 'type' => 'text', 'label' => 'Description' },
# 'order_items' => {
# 'type' => 'has_many', 'label' => 'Order items',
# 'is_association' => true, 'sort_disabled' => true, 'is_through' => false,
# 'has_scope' => false, 'foreign_key' => 'order_item_ids',
# 'class' => Order::Item
# },
# 'orders' => {
# 'type' => 'has_many', 'label' => 'Orders',
# 'is_association' => true, 'sort_disabled' => true, 'is_through' => true,
# 'has_scope' => false, 'foreign_key' => 'order_ids', 'class' => Order
# }
# }
NOTE:
index_fieldshas the same values asshow_fieldsandform_fieldsat the very first beginning. However, changing any of theindex_fields,show_fieldsorform_fieldswill not update the others.
It’s possible to update existing metadata for origin database fields. For example, to change description’s type to built-in type :raw:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.index_fields[:description][:type] = :raw
end
Or change description’s type to custom type :custom_description and create the required type partial accordingly:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.index_fields[:description][:type] = :custom_description
end
In order for custom type :custom_description to work, a type partial must be created as below:
<%# app/views/admin/products/index/_custom_description.html.erb %>
<code><%= raw value %></code>
See Type Partial to learn more about how type connects with type partial and how to create one.
It’s also possible to create metadata for custom fields. For example, to set up metadata for custom method slug:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def slug
# e.g. lev-28jn_harman-nude-leather-pointed-tue-stiletto-heel
[uid, name].join('_').downcase.dasherize
end
# NOTE: as minimum requirement, custom metadata must contain at least a value
# for `type`.
self.index_fields[:slug] = { type: 'string' }
end
See index_field_names to learn how to make a custom field (e.g.
slug) appear on index page.
:sort_field_name
NOTE: this option currently only works for
ActiveRecordmodels.
:sort_field_name is a setting that allows dev to define what field to be sorted when sorting is clicked on index page.
Take slug as example, slug is not an origin database field, therefore, by default, slug is not sortable. But to make this field sortable by name, it goes:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
# `name` is origin database field
self.index_fields[:slug][:sort_field_name] = 'name'
end
:sort_disabled
NOTE: this option currently only works for
ActiveRecordmodels.
:sort_disabled is a setting that disable/enable sorting for a field on index page. It defaults to nil, and its valid values are nil, false and true.
Take the name as example, to disable sorting for this field, it goes:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.index_fields[:name][:sort_disabled] = true
end
show_fields
show_fields is used for storing metadata of different (origin/custom) fields for the show page. For example, for model Product:
# app/models/product.rb
class Product < ApplicationRecord
has_many :order_items, class_name: Order::Item.name
has_many :orders, through: :order_items
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
end
# NOTE: Wallaby has already generated a set of metadata using the information
# from model `Product`.
ProductDecorator.show_fields
# It looks like =>
# {
# 'id' => { 'type' => 'integer', 'label' => 'Id' },
# 'description' => { 'type' => 'text', 'label' => 'Description' },
# 'order_items' => {
# 'type' => 'has_many', 'label' => 'Order items',
# 'is_association' => true, 'sort_disabled' => true, 'is_through' => false,
# 'has_scope' => false, 'foreign_key' => 'order_item_ids',
# 'class' => Order::Item
# },
# 'orders' => {
# 'type' => 'has_many', 'label' => 'Orders',
# 'is_association' => true, 'sort_disabled' => true, 'is_through' => true,
# 'has_scope' => false, 'foreign_key' => 'order_ids', 'class' => Order
# }
# }
NOTE:
show_fieldshas the same values asindex_fieldsandform_fieldsat the very first beginning. However, changing any of theindex_fields,show_fieldsorform_fieldswill not update the others.
It’s possible to update existing metadata for origin database fields. For example, to change description’s type to built-in type :raw:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.show_fields[:description][:type] = :raw
end
Or change description’s type to custom type :custom_description and create the required type partial accordingly:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.show_fields[:description][:type] = :custom_description
end
In order for custom type :custom_description to work, a type partial must be created as below:
<%# app/views/admin/products/show/_custom_description.html.erb %>
<code><%= raw value %></code>
See Type Partial to learn more about how type connects with type partial and how to create one.
It’s also possible to create metadata for custom fields. For example, to set up metadata for custom method slug:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def slug
# e.g. lev-28jn_harman-nude-leather-pointed-tue-stiletto-heel
[uid, name].join('_').downcase.dasherize
end
# NOTE: as minimum requirement, custom metadata must contain at least a value
# for `type`.
self.show_fields[:slug] = { type: 'string' }
end
See show_field_names to learn how to make a custom field (e.g.
slug) appear on show page.
form_fields
form_fields is used for storing metadata of different (origin/custom) fields for form page. For example, for model Product:
# app/models/product.rb
class Product < ApplicationRecord
has_many :order_items, class_name: Order::Item.name
has_many :orders, through: :order_items
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
end
# NOTE: Wallaby has already generated a set of metadata using the information
# from model `Product`.
ProductDecorator.form_fields
# It looks like =>
# {
# 'id' => { 'type' => 'integer', 'label' => 'Id' },
# 'description' => { 'type' => 'text', 'label' => 'Description' },
# 'order_items' => {
# 'type' => 'has_many', 'label' => 'Order items',
# 'is_association' => true, 'sort_disabled' => true, 'is_through' => false,
# 'has_scope' => false, 'foreign_key' => 'order_item_ids',
# 'class' => Order::Item
# },
# 'orders' => {
# 'type' => 'has_many', 'label' => 'Orders',
# 'is_association' => true, 'sort_disabled' => true, 'is_through' => true,
# 'has_scope' => false, 'foreign_key' => 'order_ids', 'class' => Order
# }
# }
NOTE:
form_fieldshas the same values asindex_fieldsandshow_fieldsat the very first beginning. However, changing any of theindex_fields,show_fieldsorform_fieldswill not update the others.
It’s possible to update existing metadata for origin database fields. For example, to change description’s type to built-in type :markdown:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.form_fields[:description][:type] = :markdown
end
Or change description’s type to custom type :custom_description and create the required type partial accordingly:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.form_fields[:description][:type] = :custom_description
end
In order for custom type :custom_description to work, a type partial must be created as below:
<%# app/views/admin/products/form/_custom_description.html.erb %>
<%= form.label field_name, metadata[:label] %>
<%= form.text_area field_name, class: 'form-control', data: { init: 'summernote' } %>
<code class="preview" data-bind-to-form-name="<%= field_name %>"></code>
See Type Partial to learn more about how type connects with type partial and how to create one.
It’s also possible to create metadata for custom fields. For example, to set up metadata for custom method slug:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def slug
# e.g. lev-28jn_harman-nude-leather-pointed-tue-stiletto-heel
[uid, name].join('_').downcase.dasherize
end
# NOTE: as minimum requirement, custom metadata must contain at least a value
# for `type`.
self.form_fields[:slug] = { type: 'string' }
end
See form_field_names to learn how to make a custom field (e.g.
slug) appear on form page.
Metadata Options
The following options are the common ones used in metadata for index_fields, show_fields and form_fields.
:type metadata option
NOTE: please do NOT use the following names for type:
title,logo,header,footer,user_menu,navs,index_actions,resource_actionsandresource_navs, as they are used as the configurable partials in Frontend
:type is a mandatory option. For example, to set the :type for custom method slug:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def slug
# e.g. lev-28jn_harman-nude-leather-pointed-tue-stiletto-heel
[uid, name].join('_').downcase.dasherize
end
# NOTE: as minimum requirement, custom metadata must contain at least a value
# for `type`.
self.index_fields[:slug] = { type: 'string' }
end
:label metadata option
:label is to customize text for the field. For example, to change text of the field uid to SKU:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.index_fields[:uid]
# it looks like =>
# { type: 'string', label: 'Uid' }
self.index_fields[:uid][:label] = 'SKU'
end
Metadata Options for Association Fields
Wallaby pre-generates metadata for association fields that can be found in the model.
belongs_to
For ActiveRecord model, it looks like:
# app/models/product.rb class Product < ApplicationRecord belongs_to :category end # app/decorators/product_decorator.rb class ProductDecorator < Admin::ApplicationDecorator end ProductDecorator.index_fields[:category] # It looks like => # { # 'type' => 'belongs_to', 'label' => 'Category', # 'is_association' => true, 'sort_disabled' => true, # 'is_through' => false, 'has_scope' => false, # 'foreign_key' => 'category_id', 'class' => Category # }
has_one
For ActiveRecord model, it looks like:
# app/models/product.rb class Product < ApplicationRecord has_one :picture end # app/decorators/product_decorator.rb class ProductDecorator < Admin::ApplicationDecorator end ProductDecorator.index_fields[:picture] # It looks like => # { # 'type' => 'has_one', 'label' => 'Picture', # 'is_association' => true, 'sort_disabled' => true, # 'is_through' => false, 'has_scope' => true, # 'foreign_key' => 'picture_id', 'class' => Picture # }
has_many
For ActiveRecord model, it looks like:
# app/models/product.rb class Product < ApplicationRecord has_many :order_items, class_name: Order::Item.name has_many :orders, through: :order_items end # app/decorators/product_decorator.rb class ProductDecorator < Admin::ApplicationDecorator end ProductDecorator.index_fields[:orders] # It looks like => # { # 'type' => 'has_many', 'label' => 'Orders', # 'is_association' => true, 'sort_disabled' => true, # 'is_through' => true, 'has_scope' => false, # 'foreign_key' => 'order_ids', 'class' => Order # }
has_and_belongs_to_many
For ActiveRecord model, it looks like:
# app/models/product.rb class Product < ApplicationRecord has_and_belongs_to_many :tags end # app/decorators/product_decorator.rb class ProductDecorator < Admin::ApplicationDecorator end ProductDecorator.index_fields[:tags] # It looks like => # { # 'type' => 'has_and_belongs_to_many', 'label' => 'Tags', # 'is_association' => true, 'sort_disabled' => true, # 'is_through' => false, 'has_scope' => false, # 'foreign_key' => 'tag_ids', 'class' => Tag # }
Polymorphic Association
For ActiveRecord model, it looks like:
# app/models/picture.rb class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end # app/decorators/picture_decorator.rb class PictureDecorator < Admin::ApplicationDecorator end PictureDecorator.index_fields[:imageable] # It looks like => # { # 'type' => 'belongs_to', 'label' => 'Imageable', # 'is_association' => true, 'sort_disabled' => true, # 'is_polymorphic' => true, 'is_through' => false, 'has_scope' => false, # 'foreign_key' => 'imageable_id', 'polymorphic_type' => 'imageable_type', # 'polymorphic_list' => [Product] # }
index_field_names
index_field_names is used to get the configured fields displayed on index page. For example, for model Product:
# app/models/product.rb
class Product < ApplicationRecord
has_many :order_items, class_name: Order::Item.name
has_many :orders, through: :order_items
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
# NOTE: Wallaby has already generated the list of fields from model `Product`.
self.index_field_names
# It looks like =>
# [
# 'id', 'sku', 'name', 'stock', 'price', 'featured',
# 'available_to_date', 'available_to_time', 'published_at'
# ]
end
NOTE: by default, for
ActiveRecordmodels,index_field_namesexcludes association fields and big-value fields (e.g. binary, blob, citext, hstore, json, jsonb, longblob, longtext, mediumblob, mediumtext, text, tsvector, xml).
Because the index_field_names is an array, to modify the values, all array functions can be used. For example, to add a custom field slug to it:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def slug
# e.g. lev-28jn_harman-nude-leather-pointed-tue-stiletto-heel
[uid, name].join('_').downcase.dasherize
end
self.index_fields[:slug] = { type: 'string' }
self.index_field_names << 'slug'
end
Or replace index_field_names with a new array. For example:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.index_field_names = ['id', 'name', 'price', 'created_at']
end
Then frontend will render the fields in array order accordingly.
show_field_names
show_field_names is used to get the configured fields displayed on show page. For example, for model Product:
# app/models/product.rb
class Product < ApplicationRecord
has_many :order_items, class_name: Order::Item.name
has_many :orders, through: :order_items
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
# NOTE: Wallaby has already generated the list of fields from model `Product`.
self.show_field_names
# It looks like =>
# [
# 'id', 'sku', 'name', 'stock', 'price', 'featured',
# 'available_to_date', 'available_to_time', 'published_at',
# 'orders', 'order_items'
# ]
end
NOTE: unlike
index_field_namesandform_field_names, by default,show_field_namesdoes NOT exclude any fields from displaying.
Because the show_field_names is an array, to modify the values, all array functions can be used. For example, to add a custom field slug to it:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def slug
# e.g. lev-28jn_harman-nude-leather-pointed-tue-stiletto-heel
[uid, name].join('_').downcase.dasherize
end
self.show_fields[:slug] = { type: 'string' }
self.show_field_names << 'slug'
end
Or replace show_field_names with a new array. For example:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.show_field_names = ['id', 'name', 'price', 'created_at']
end
Then frontend will render the fields in array order accordingly.
form_field_names
form_field_names is used to get the configured fields displayed on form (new/edit) page. For example, for model Product:
# app/models/product.rb
class Product < ApplicationRecord
has_many :order_items, class_name: Order::Item.name
has_many :orders, through: :order_items
end
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
# NOTE: Wallaby has already generated the list of fields from model `Product`.
self.form_field_names
# It looks like =>
# [
# 'id', 'sku', 'name', 'stock', 'price', 'featured',
# 'available_to_date', 'available_to_time', 'published_at',
# 'orders', 'order_items'
# ]
end
NOTE: by default, for
ActiveRecordmodels,form_field_namesexcludes primary key field, default timestamp fields (e.g.created_atandupdated_at) and the associations that have custom scope (e.g.has_many :special_orders, -> { special: true }) or have:throughoption (e.g.has_many :orders, through: :order_items).
Because the form_field_names is an array, to modify the values, all array functions can be used. For example, to add a custom field name to it:
# app/models/customer.rb
class Customer < ApplicationRecord
def name
[first_name, last_name].compact.join ' '
end
def name=(name)
names = name.rpartition(' ')
self.first_name = names.first
self.last_name = names.last
end
end
# app/decorators/customer_decorator.rb
class CustomerDecorator < Admin::ApplicationDecorator
self.form_fields[:name] = { type: 'string' }
self.form_field_names << 'name'
end
Or replace form_field_names with a new array. For example:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.form_field_names = ['id', 'name', 'price', 'created_at']
end
Then frontend will render the fields in array order accordingly.
Create Filters
Filters are predefined queries on index page that allows user to click.
Learn more about how filter is working behind the scene, take a look at the Servicer
With Existing Named Scope
If there is existing scope, such as:
# app/model/product.rb
class Product < ApplicationRecord
scope :red, -> { where(color: 'red') }
end
Then filter can be created as below:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.filters[:red] = {
scope: :red,
label: 'Red Products'
}
# or with minimum metadata if not much customization is needed
self.filters[:red] = {}
end
With Custom Scape
Defining a filter with custom scope in a decorator is easy as in ActiveRecord model:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.filters[:blue] = {
scope: -> { where(color: 'blue') }
}
end
Options
Here are the options for defining a filter
:scope filter option
:scope option is to customize the query. It accepts the following values:
symbol- the value should be the symbol name of the query that have defined in the model. For example:# app/model/product.rb class Product < ApplicationRecord scope :red, -> { where(color: 'red') } end # app/decorators/product_decorator.rb class ProductDecorator < Admin::ApplicationDecorator self.filters[:red] = { scope: :red } endproc- a proc similar to how a scope is defined in a model, all model class methods can be accessed inside the proc. For example:# app/decorators/product_decorator.rb class ProductDecorator < Admin::ApplicationDecorator self.filters[:red] = { scope: -> { where(color: 'red') } } end
:label
:label is to customize the text that user reads on the index page. For example, to configure the red filter’s label as Hot Red Products:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.filters[:red] = { label: 'Hot Red Products' }
end
:default
:default will mark the query to be run when user first time landing on the page. To customize it, it goes:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
self.filters[:red] = { default: true }
end
Misc Customization
to_label
To customize the title of a resource used by show page and form autocomplete:
# app/decorators/product_decorator.rb
class ProductDecorator < Admin::ApplicationDecorator
def to_label
product_slug + ' - ' + uuid
end
end
primary_key
If a model has no primary key, in order to make show/edit page working properly, it’s required to specify the field to be used as primary key. For example:
# app/decorators/active_record/schema_migration_decorator.rb
class ActiveRecord::SchemaMigrationDecorator < Admin::ApplicationDecorator
self.primary_key = 'version'
end