Display Module
The Display module provides a comprehensive system for rendering and displaying data values in Plutonium applications. Built on top of Phlexi::Display
, it offers specialized components for different data types, consistent theming, and intelligent value rendering.
TIP
The Display module is located in lib/plutonium/ui/display/
.
Overview
- Value Rendering: Intelligent rendering of different data types.
- Specialized Components: Purpose-built components for associations, attachments, markdown, etc.
- Theme System: Consistent styling across all display components.
- Type Inference: Automatic component selection based on data types.
- Resource Integration: Seamless integration with resource definitions.
- Responsive Design: Mobile-first responsive display layouts.
Core Components
Base Display (lib/plutonium/ui/display/base.rb
)
This is the foundation that all display components inherit from. It extends Phlexi::Display::Base
with Plutonium's specific behaviors and custom display components.
Base Display Implementation
class Plutonium::UI::Display::Base < Phlexi::Display::Base
include Plutonium::UI::Component::Behaviour
# Enhanced builder with Plutonium-specific components
class Builder < Builder
include Plutonium::UI::Display::Options::InferredTypes
def association_tag(**options, &block)
create_component(Plutonium::UI::Display::Components::Association, :association, **options, &block)
end
def markdown_tag(**options, &block)
create_component(Plutonium::UI::Display::Components::Markdown, :markdown, **options, &block)
end
def attachment_tag(**options, &block)
create_component(Plutonium::UI::Display::Components::Attachment, :attachment, **options, &block)
end
def phlexi_render_tag(**options, &block)
create_component(Plutonium::UI::Display::Components::PhlexiRender, :phlexi_render, **options, &block)
end
alias_method :phlexi_tag, :phlexi_render_tag
end
end
Resource Display (lib/plutonium/ui/display/resource.rb
)
This is a specialized component for displaying resource objects, automatically rendering fields and associations based on the resource's definition.
class PostDisplay < Plutonium::UI::Display::Resource
def initialize(post, resource_fields:, resource_associations:, resource_definition:)
super(
post,
resource_fields: resource_fields,
resource_associations: resource_associations,
resource_definition: resource_definition
)
end
def display_template
render_fields # Render configured fields
render_associations if present_associations? # Render associations
end
end
Display Components
Association Component
Renders associated objects with automatic linking to the resource's show page if it's a registered resource.
# Automatically used for association fields
render field(:author).association_tag
class Plutonium::UI::Display::Components::Association
def render_value(value)
if registered_resources.include?(value.class)
# Create link to resource
href = resource_url_for(value, parent: appropriate_parent)
a(class: themed(:link), href: href) { display_name_of(value) }
else
# Plain text display
display_name_of(value)
end
end
end
Attachment Component
Provides a rich display for file attachments with thumbnails for images and icons for other file types.
# Automatically used for attachment fields
render field(:featured_image).attachment_tag
render field(:documents).attachment_tag(caption: false)
render field(:gallery).attachment_tag(
caption: ->(attachment) { attachment.description }
)
Attachment Component Implementation
class Plutonium::UI::Display::Components::Attachment
def render_value(attachment)
div(
class: "attachment-preview",
data: {
controller: "attachment-preview",
attachment_preview_mime_type_value: attachment.content_type,
attachment_preview_thumbnail_url_value: attachment_thumbnail_url(attachment)
}
) do
render_thumbnail(attachment) # Image or file type icon
render_caption(attachment) # Filename or custom caption
end
end
private
def render_thumbnail(attachment)
if attachment.representable?
img(src: attachment_thumbnail_url(attachment), class: "w-full h-full object-cover")
else
# File type icon
div(class: "file-icon") { ".#{attachment_extension(attachment)}" }
end
end
end
Markdown Component
Securely renders markdown content with syntax highlighting for code blocks.
# Automatically used for :markdown fields
render field(:description).markdown_tag
class Plutonium::UI::Display::Components::Markdown
RENDERER = Redcarpet::Markdown.new(
Redcarpet::Render::HTML.new(
safe_links_only: true,
with_toc_data: true,
hard_wrap: true,
link_attributes: { rel: :nofollow, target: :_blank }
),
autolink: true,
tables: true,
fenced_code_blocks: true,
strikethrough: true,
footnotes: true
)
def render_value(value)
article(class: themed(:markdown)) do
raw(safe(render_markdown(value)))
end
end
end
PhlexiRender Component
Renders a given value using a custom Phlex component, allowing for complex, specialized displays.
# Render with conditional logic
render field(:chart_data) do |f|
if f.value.present?
render ChartComponent.new(data: f.value, class: f.dom.css_class)
else
span(class: "text-gray-500") { "No chart data" }
end
end
# Simple component rendering
render field(:status_badge) do |f|
render StatusBadgeComponent.new(status: f.value, class: f.dom.css_class)
end
class Plutonium::UI::Display::Components::PhlexiRender
def render_value(value)
phlexi_render(build_phlexi_component(value)) do
# Fallback rendering if component fails
p(class: themed(:string)) { value }
end
end
private
def build_phlexi_component(value)
@builder.call(value, attributes)
end
end
Type Inference
The display system automatically selects the appropriate component based on the field's type, but you can always override it manually.
# Based on Active Record column types or Active Storage attachments
render field(:title).string_tag # -> :string
render field(:content).text_tag # -> :text
render field(:published_at).datetime_tag # -> :datetime
render field(:author).association_tag # -> :association
render field(:featured_image).attachment_tag # -> :attachment
render field(:description).markdown_tag # -> :markdown (if configured in definition)
render field(:title).string_tag
render field(:content).markdown_tag
render field(:author).association_tag
Type Mapping Implementation
module Plutonium::UI::Display::Options::InferredTypes
private
def infer_field_component
case inferred_field_type
when :attachment
:attachment
when :association
:association
when :boolean
:boolean
# ... and so on for all standard types
else
:string
end
end
end
Theme System
Display Theme (lib/plutonium/ui/display/theme.rb
)
Comprehensive theming for consistent visual appearance:
class Plutonium::UI::Display::Theme < Phlexi::Display::Theme
def self.theme
super.merge({
# Layout
fields_wrapper: "p-6 grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-6 gap-y-10 grid-flow-row-dense",
value_wrapper: "max-h-[300px] overflow-y-auto",
# Typography
label: "text-base font-bold text-gray-500 dark:text-gray-400 mb-1",
string: "text-md text-gray-900 dark:text-white mb-1 whitespace-pre-line",
text: "text-md text-gray-900 dark:text-white mb-1 whitespace-pre-line",
# Interactive elements
link: "text-primary-600 dark:text-primary-500 whitespace-pre-line",
email: "flex items-center text-md text-primary-600 dark:text-primary-500 mb-1",
phone: "flex items-center text-md text-primary-600 dark:text-primary-500 mb-1",
# Special content
markdown: "format dark:format-invert format-primary",
json: "text-sm text-gray-900 dark:text-white mb-1 whitespace-pre font-mono shadow-inner p-4",
# Attachments
attachment_value_wrapper: "grid grid-cols-[repeat(auto-fill,minmax(0,180px))]",
# Colors
color: "flex items-center text-md text-gray-900 dark:text-white mb-1",
color_indicator: "w-10 h-10 rounded-full mr-2"
})
end
end
Table Display Theme (lib/plutonium/ui/table/display_theme.rb
)
Specialized theming for table contexts:
class Plutonium::UI::Table::DisplayTheme < Phlexi::Table::DisplayTheme
def self.theme
super.merge({
# Compact display for tables
value_wrapper: "max-h-[150px] overflow-y-auto",
prefixed_icon: "w-4 h-4 mr-1",
# Table-specific styles
email: "flex items-center text-primary-600 dark:text-primary-500 whitespace-nowrap",
phone: "flex items-center text-primary-600 dark:text-primary-500 whitespace-nowrap",
attachment_value_wrapper: "flex flex-wrap gap-1"
})
end
end
Usage Patterns
Basic Display
# Simple field display
class PostDisplay < Plutonium::UI::Display::Base
def display_template
render field(:title).string_tag
render field(:content).text_tag
render field(:published_at).datetime_tag
render field(:author).association_tag
end
end
Field Rendering and Wrappers
Fields must be explicitly rendered using the render
method. You can also use wrappers to control the layout and styling:
class PostDisplay < Plutonium::UI::Display::Base
def display_template
# Basic field rendering
render field(:title).string_tag
# Field with wrapper options
render field(:content).wrapped(class: "col-span-full prose") do |f|
render f.markdown_tag
end
# Field with custom wrapper and styling
render field(:author).wrapped(
class: "border rounded-lg p-4",
data: { controller: "tooltip" }
) do |f|
render f.association_tag
end
# Multiple fields with consistent wrapper
[:created_at, :updated_at].each do |field_name|
render field(field_name).wrapped(class: "text-sm text-gray-500") do |f|
render f.datetime_tag
end
end
end
end
Resource Display
# Automatic resource display based on definition
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
@display = Plutonium::UI::Display::Resource.new(
@post,
resource_fields: current_definition.defined_displays.keys,
resource_associations: [],
resource_definition: current_definition
)
end
end
# In view
<%= render @display %>
Custom Display Components
# Create custom display component
class StatusBadgeComponent < Plutonium::UI::Component::Base
def initialize(status, **options)
@status = status
@options = options
end
def view_template
span(class: badge_classes) { @status.humanize }
end
private
def badge_classes
base_classes = "px-2 py-1 text-xs font-medium rounded-full"
case @status
when 'active'
"#{base_classes} bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
when 'inactive'
"#{base_classes} bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
else
"#{base_classes} bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"
end
end
end
# Use in display
render field(:status) do |f|
render StatusBadgeComponent.new(status: f.value)
end
Conditional Display
You can conditionally show or hide display fields using the :condition
option in your resource definition. This is useful for creating dynamic views that adapt to the state of your data.
Note: Conditional display is for cosmetic or state-based logic. For controlling data visibility based on user roles or permissions, use policies.
# app/definitions/post_definition.rb
class PostDefinition < Plutonium::Resource::Definition
# Show a field only when the object is in a certain state.
display :published_at, condition: -> { object.published? }
display :reason_for_rejection, condition: -> { object.rejected? }
display :scheduled_for, condition: -> { object.scheduled? }
# Show a field based on the object's attributes.
display :comments, condition: -> { object.comments_enabled? }
# Show debug information only in development.
display :debug_info, condition: -> { Rails.env.development? }
end
Condition Context
condition
procs for display
fields are evaluated in the display rendering context, which means they have access to:
object
- The record being displayed- All helper methods available in the display context
This allows for dynamic field visibility based on the record's state or other contextual information.
You can also implement custom conditional logic by overriding the rendering methods:
class PostDisplay < Plutonium::UI::Display::Resource
private
def render_resource_field(name)
# Only render if user has permission
when_permitted(name) do
# Get field and display options from definition
field_options = resource_definition.defined_fields[name]&.dig(:options) || {}
display_definition = resource_definition.defined_displays[name] || {}
display_options = display_definition[:options] || {}
# Render field with appropriate component
field(name, **field_options).wrapped(**wrapper_options) do |f|
render_field_component(f, display_options)
end
end
end
def when_permitted(name, &block)
return unless @resource_fields.include?(name)
return unless policy_allows_field?(name)
yield
end
end
Responsive Layouts
# Grid layout with responsive columns
class PostDisplay < Plutonium::UI::Display::Base
private
def fields_wrapper(&block)
div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6") do
yield
end
end
end
# Full-width fields
render field(:description).wrapped(class: "col-span-full") do |f|
render f.markdown_tag
end
# Compact display
render field(:tags).wrapped(class: "col-span-1") do |f|
render f.collection_tag
end
Helper Integration
Display Helpers (lib/plutonium/helpers/display_helper.rb
)
Rich helper methods for value formatting:
module Plutonium::Helpers::DisplayHelper
# Generic field display with helper support
def display_field(value:, helper: nil, **options)
return "-" unless value.present?
if value.respond_to?(:each) && stack_multiple
# Handle collections
tag.ul(class: "list-unstyled") do
value.each do |val|
concat tag.li(display_field_value(value: val, helper: helper))
end
end
else
display_field_value(value: value, helper: helper, **options)
end
end
# Specialized display methods
def display_association_value(association)
display_name = display_name_of(association)
if registered_resources.include?(association.class)
link_to display_name, resource_url_for(association),
class: "font-medium text-primary-600 dark:text-primary-500"
else
display_name
end
end
def display_datetime_value(value)
timeago(value)
end
def display_boolean_value(value)
tag.input(type: :checkbox, checked: value, disabled: true)
end
def display_name_of(obj, separator: ", ")
return unless obj.present?
return obj.map { |i| display_name_of(i) }.join(separator) if obj.is_a?(Array)
# Try common display methods
%i[to_label name title].each do |method|
name = obj.public_send(method) if obj.respond_to?(method)
return name if name.present?
end
# Fallback for Active Record objects
return "#{resource_name(obj.class)} ##{obj.id}" if obj.respond_to?(:id)
obj.to_s
end
end
Advanced Features
Attachment Previews
# Automatic attachment preview with JavaScript enhancement
field(:documents).attachment_tag
# Generates:
# - Thumbnail images for representable files
# - File type indicators for non-representable files
# - Click-to-preview functionality
# - Download links
# - Responsive grid layout
# JavaScript controller provides:
# - Preview modal/lightbox
# - Keyboard navigation
# - Touch/swipe support
# - Loading states
Markdown Processing
# Secure markdown with custom renderer
class CustomMarkdownRenderer < Redcarpet::Render::HTML
def initialize(options = {})
super(options.merge(
safe_links_only: true,
with_toc_data: true,
hard_wrap: true,
link_attributes: { rel: :nofollow, target: :_blank }
))
end
def block_code(code, language)
# Custom syntax highlighting
"<pre><code class=\"language-#{language}\">#{highlight_code(code, language)}</code></pre>"
end
end
# Use custom renderer
CUSTOM_RENDERER = Redcarpet::Markdown.new(
CustomMarkdownRenderer.new,
autolink: true,
tables: true,
fenced_code_blocks: true
)
Performance Optimizations
# Lazy loading for expensive displays
class PostDisplay < Plutonium::UI::Display::Base
def display_template
render field(:title).string_tag
# Only render associations if not in turbo frame
if current_turbo_frame.nil?
render field(:comments_count).number_tag
render field(:recent_comments).collection_tag
end
end
end
# Conditional rendering based on permissions
def render_resource_field(name)
return unless authorized_to_view_field?(name)
# Cache expensive field computations
@field_cache ||= {}
@field_cache[name] ||= compute_field_display(name)
render @field_cache[name]
end
Testing
Component Testing
RSpec.describe Plutonium::UI::Display::Components::Association do
let(:user) { create(:user, name: "John Doe") }
let(:component) { described_class.new(field_for(user, :author)) }
context "when association is a registered resource" do
before { allow(component).to receive(:registered_resources).and_return([User]) }
it "renders a link to the resource" do
html = render(component)
expect(html).to include('href="/users/')
expect(html).to include("John Doe")
end
end
context "when association is not registered" do
before { allow(component).to receive(:registered_resources).and_return([]) }
it "renders plain text" do
html = render(component)
expect(html).not_to include('href=')
expect(html).to include("John Doe")
end
end
end
Integration Testing
RSpec.describe "Display Integration", type: :system do
let(:post) { create(:post, :with_attachments, :with_author) }
it "displays all field types correctly" do
visit post_path(post)
# Text fields
expect(page).to have_content(post.title)
expect(page).to have_content(post.content)
# Associations
expect(page).to have_link(post.author.name, href: user_path(post.author))
# Attachments
expect(page).to have_css(".attachment-preview")
expect(page).to have_link(href: rails_blob_path(post.featured_image))
# Timestamps
expect(page).to have_content("ago") # timeago formatting
end
it "handles responsive layout" do
visit post_path(post)
# Desktop layout
expect(page).to have_css(".md\\:grid-cols-2")
# Mobile layout (resize viewport)
page.driver.browser.manage.window.resize_to(375, 667)
expect(page).to have_css(".grid-cols-1")
end
end
Best Practices
Component Design
- Single Responsibility: Each component should handle one display type
- Consistent API: Follow the same patterns for all display components
- Theme Integration: Use themed classes for consistent styling
- Accessibility: Include proper ARIA attributes and semantic HTML
- Performance: Avoid expensive operations in render methods
Value Processing
- Null Safety: Always handle nil/empty values gracefully
- Type Checking: Verify value types before processing
- Sanitization: Sanitize user-generated content (especially HTML/markdown)
- Formatting: Use consistent formatting for dates, numbers, etc.
- Localization: Support internationalization for display text
Responsive Design
- Mobile First: Design for mobile, enhance for desktop
- Flexible Layouts: Use CSS Grid/Flexbox for adaptive layouts
- Content Priority: Show most important content first on small screens
- Touch Friendly: Ensure interactive elements are touch-accessible
- Performance: Optimize images and assets for different screen sizes
Security
- Input Sanitization: Always sanitize user-generated content
- XSS Prevention: Use safe HTML rendering methods
- Link Security: Add
rel="nofollow"
to user-generated links - File Security: Validate file types and sizes for attachments
- Permission Checks: Verify user permissions before displaying sensitive data