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::ApplicationDecorator is taken for other purpose.

Starting with:

Configuration can be set for:

Accessing helper methods:

  • #resource - to access the resource object.
  • #h - to access the Rails and Wallaby helpers.

Configuring the metadata for fields:

Common options for metadata:

Configuring the fields to display:

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_fields has the same values as show_fields and form_fields at the very first beginning. However, changing any of the index_fields, show_fields or form_fields will 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 ActiveRecord models.

: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 ActiveRecord models.

: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_fields has the same values as index_fields and form_fields at the very first beginning. However, changing any of the index_fields, show_fields or form_fields will 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_fields has the same values as index_fields and show_fields at the very first beginning. However, changing any of the index_fields, show_fields or form_fields will 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_actions and resource_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 ActiveRecord models, index_field_names excludes 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_names and form_field_names, by default, show_field_names does 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 ActiveRecord models, form_field_names excludes primary key field, default timestamp fields (e.g. created_at and updated_at) and the associations that have custom scope (e.g. has_many :special_orders, -> { special: true }) or have :through option (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 }
    end
    
  • proc - 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