Chapter 8: Customizing the UI
In this chapter, you'll customize forms, tables, and pages to create a polished interface.
Customizing Fields
Fields control how attributes appear in forms and displays. Plutonium auto-infers fields from your model, so you only need to declare fields when customizing their behavior.
Field Types
# packages/blogging/app/definitions/blogging/post_definition.rb
class Blogging::PostDefinition < Blogging::ResourceDefinition
# Rich text editor instead of plain textarea
field :body, as: :markdown
# Select with predefined options
input :status, as: :select, choices: %w[draft review published]
endConditional Fields
Show fields based on conditions:
# Only show published_at when published is true
field :published_at, condition: -> { object.published? }
# Show different fields for new vs existing records
field :author, condition: :new_record?Customizing Tables
Columns are auto-inferred from your model. Only declare columns when customizing their behavior.
Column Configuration
# Custom label
column :user, label: "Author"
# Computed column with block
column :comment_count do |post|
post.comments.count
endTable Actions
# Show page actions
action :publish, interaction: Blogging::PublishPost, record_action: true
# Table row actions
action :archive, interaction: Blogging::ArchivePost, collection_record_action: true
# Index page actions
action :import, interaction: Blogging::ImportPosts, resource_action: true
# Bulk actions (selected records)
action :bulk_publish, interaction: Blogging::BulkPublish, bulk_action: trueCustomizing Search and Filters
# Search configuration
search do |scope, query|
scope.where("title ILIKE ? OR body ILIKE ?", "%#{query}%", "%#{query}%")
end
# Predefined scopes (reference model scopes)
scope :published, default: true # Applied by default, uses Post.published
scope :drafts # Uses Post.drafts
# Inline scope with block
scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
# Inline scope with controller context
scope(:mine) { |scope| scope.where(user: current_user) }
# Filters
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
# Custom filter with lambda
filter :published, with: ->(scope, value) {
value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
}
# Sorting options
sort :title
sort :created_at
sort :published
# Default sort
default_sort :created_at, :descCustom Page Classes
Override page title and description in definitions:
class Blogging::PostDefinition < Blogging::ResourceDefinition
# Custom page titles
index_page_title "Blog Posts"
index_page_description "Manage your blog content"
show_page_title { |record| record.title }
show_page_description "View post details"
endFor more advanced customization, you can create custom page classes that inherit from Plutonium's page components:
# packages/admin_portal/app/views/admin_portal/blogging/posts/index_page.rb
class AdminPortal::Blogging::Posts::IndexPage < Blogging::PostDefinition::IndexPage
private
def page_title
"Blog Posts"
end
def page_description
"Manage your blog content"
end
# Add content after the page header
def render_after_page_header
div(class: "mb-4 p-4 bg-blue-50 rounded") do
p { "Custom content here" }
end
end
endCustom Form Layout
Control form layout using wrapper options in definitions:
class Blogging::PostDefinition < Blogging::ResourceDefinition
# Full-width fields
input :title, wrapper: {class: "col-span-full"}
input :body, as: :markdown, wrapper: {class: "col-span-full"}
# Side-by-side fields (default is col-span-full)
input :published_at, wrapper: {class: "col-span-1"}
input :category, wrapper: {class: "col-span-1"}
endFor advanced form customization, use the block syntax:
input :birth_date do |f|
f.date_tag(min: 18.years.ago.to_date)
endTheming with TailwindCSS
Plutonium uses TailwindCSS 4. Customize the theme:
/* app/assets/stylesheets/application.css */
@import "tailwindcss";
@import "gem:plutonium/src/css/plutonium.css";
@theme {
--color-primary-500: #6366f1; /* Indigo */
--color-primary-600: #4f46e5;
--color-primary-700: #4338ca;
--radius-md: 0.5rem;
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}Custom Components
Create reusable components with Phlex:
# app/components/status_badge.rb
class StatusBadge < Plutonium::UI::Component::Base
def initialize(published:)
@published = published
end
def view_template
if @published
span(class: "px-2 py-1 text-xs bg-green-100 text-green-800 rounded") { "Published" }
else
span(class: "px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded") { "Draft" }
end
end
end
# Use in definition
column :status do |post|
render StatusBadge.new(published: post.published?)
endLayout Customization
Layouts are Phlex components that wrap page content. The base layout provides hooks for customization:
class CustomLayout < Plutonium::UI::Layout::ResourceLayout
private
# Customize body classes
def body_attributes
{class: "antialiased min-h-screen bg-white dark:bg-gray-900"}
end
# Add content before the main section
def render_before_main
super
# Add custom header content
end
# Add content after the main section
def render_after_main
super
# Add custom footer content
end
endSee the Theming Guide for comprehensive customization options.
What's Next
Congratulations! You've built a complete blog application with:
- Resource CRUD operations
- Authentication with Rodauth
- Authorization with policies
- Custom actions with Interactions
- Nested resources
- Multiple portals (Admin and Author)
- Customized UI
Continue exploring:
Happy building with Plutonium!
