Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document and clean up helper modules #474

Merged
merged 27 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
632ca14
Minimize mocking in FormatHelper spec
spohlenz Aug 21, 2024
27a22ce
Add spec for IconHelper
spohlenz Aug 21, 2024
ae37144
Add spec for StatusHelper
spohlenz Aug 21, 2024
3f4617a
Update use of content_tag within helpers to use Rails' tag builder
spohlenz Aug 21, 2024
3aa995a
Add spec for CardHelper and use keyword arguments where appropriate
spohlenz Aug 21, 2024
a7f5de0
Add spec for TimestampHelper and use keyword arguments where appropriate
spohlenz Aug 21, 2024
89e693e
Remove ActionView::Context from specs where not required
spohlenz Aug 21, 2024
0a9cd7b
Add specs for Gravatar helpers and extract as own module
spohlenz Aug 21, 2024
c989f89
Remove turbo_stream_update_flash helper
spohlenz Aug 21, 2024
27663c8
Use semantic tags within card helper and add comments
spohlenz Aug 22, 2024
007147a
Add spec and docs for ContainerHelper
spohlenz Aug 23, 2024
a1c841c
Clean up table helper with kwargs
spohlenz Aug 23, 2024
1994bfb
Update Bootstrap URLs in GridHelper docs
spohlenz Aug 23, 2024
002eeaf
Add documentation for avatar, gravatar, heading and icon helpers
spohlenz Aug 23, 2024
cac08ce
Mark internal helper modules
spohlenz Aug 24, 2024
ce7a726
Minor reorganization of helper method locations
spohlenz Aug 24, 2024
8a0b849
Improve use of kwargs for sort_link helper
spohlenz Aug 25, 2024
c3967b1
Add spec and docs for ModalHelper
spohlenz Aug 25, 2024
163cc50
Add docs for sort_link helper and fix kwargs forwarding from Column
spohlenz Aug 25, 2024
1e0c6c3
Remove unneeded Rails version check in params helper specs
spohlenz Aug 25, 2024
6457102
Explicitly limit sort_options when calling sort_link for column header
spohlenz Aug 25, 2024
f4bbdef
Clean up FormatHelper (remove separate format_value_from_options method)
spohlenz Aug 25, 2024
c5d038c
Add spec and docs for FormHelper
spohlenz Aug 26, 2024
8810af9
Use kwargs options for tabs
spohlenz Aug 26, 2024
38d1549
Use kwargs in admin_url_for helper
spohlenz Aug 26, 2024
4c2be3c
Add documentation for remaining public helpers
spohlenz Aug 26, 2024
9805806
Make use of rspec-rails helper spec type
spohlenz Aug 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions app/helpers/trestle/avatar_helper.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
module Trestle
module AvatarHelper
def avatar(options={})
fallback = options.delete(:fallback) if options[:fallback]

content_tag(:div, default_avatar_options.merge(options)) do
concat content_tag(:span, fallback, class: "avatar-fallback") if fallback
# Renders an avatar (or similar image) styled as a circle typically designed to
# represent a user (though not limited to that). The avatar helper accepts a block
# within which the image should be provided, which will be resized to fit.
#
# fallback - Optional short text (2-3 characters) shown when no image is available
# attributes - Additional HTML attributes to add to the <div> tag
#
# Examples
#
# <%= avatar { image_tag("person.jpg") } %>
#
# <%= avatar(fallback: "SP", class: "avatar-lg") { gravatar("[email protected]") } %>
#
# <%= avatar(style: "--avatar-size: 3rem") { gravatar("[email protected]") }
#
# Return the HTML div containing the avatar image.
def avatar(fallback: nil, **attributes, &block)
tag.div(**default_avatar_options.merge(attributes)) do
concat tag.span(fallback, class: "avatar-fallback") if fallback
concat yield if block_given?
end
end

def gravatar(email, options={})
options = { d: "mp" }.merge(options)

url = "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email.to_s.downcase)}.png"
url << "?#{options.to_query}" if options.any?

image_tag(url)
end

protected
def default_avatar_options
Trestle::Options.new(class: ["avatar"])
end
Expand Down
36 changes: 27 additions & 9 deletions app/helpers/trestle/card_helper.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
module Trestle
module CardHelper
def card(options={}, &block)
content_tag(:div, options.slice(:id, :data).merge(class: ["card", options[:class]].compact)) do
# Renders a card container element (sometimes also known as a panel or well), based on
# Bootstrap's card element (https://getbootstrap.com/docs/5.3/components/card/).
#
# header - Optional header to add within a .card-header
# footer - Optional footer to add within a .card-footer
# attributes - Additional HTML attributes to add to the container <div> tag
#
# Examples
#
# <%= card do %>
# <p>Card content here...</p>
# <% end %>
#
# <%= card header: "Header", footer: "Footer", class: "text-bg-primary" do %>...
#
# Returns a HTML-safe String.
def card(header: nil, footer: nil, **attributes, &block)
tag.div(**attributes.merge(class: ["card", attributes[:class]])) do
safe_join([
(content_tag(:div, options[:header], class: "card-header") if options[:header]),
content_tag(:div, class: "card-body", &block),
(content_tag(:div, options[:footer], class: "card-footer") if options[:footer])
(tag.header(header, class: "card-header") if header),
tag.div(class: "card-body", &block),
(tag.footer(footer, class: "card-footer") if footer)
].compact)
end
end

def panel(options={}, &block)
# [DEPRECATED] Alias for card
def panel(**attributes, &block)
Trestle.deprecator.warn("The panel helper is deprecated and will be removed in future versions of Trestle. Please use the card helper instead.")
card(options.merge(header: options[:title]), &block)
card(**attributes.merge(header: attributes[:title]), &block)
end

def well(options={}, &block)
# [DEPRECATED] Alias for card
def well(**attributes, &block)
Trestle.deprecator.warn("The well helper is deprecated and will be removed in future versions of Trestle. Please use the card helper instead.")
card(options, &block)
card(**attributes, &block)
end
end
end
43 changes: 37 additions & 6 deletions app/helpers/trestle/container_helper.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
module Trestle
module ContainerHelper
def container(&block)
# Renders a content container with an optional sidebar, which can
# be useful when creating an admin or dashboard with a custom view.
#
# This helper accepts a block (within which the main content is provided),
# which yields a `Context` capture object. The Context object has one important
# method -- `sidebar(**attributes, &block)` which captures the sidebar content
# to be rendered after the main content.
#
# attributes - Additional HTML attributes to add to the <div> tag
#
# Examples
#
# <%= container do |c| %>
# This content will be wrapped in a .main-content-container > .main-content div.
# <% end %>
#
# <%= container do |c| %>
# <% c.sidebar class: "order-first" %>
# Sidebar content...
# <% end %>
# Main content...
# <% end %>
#
# Returns a HTML-safe String.
def container(**attributes, &block)
context = Context.new(self)
content = capture(context, &block)
content = capture(context, &block) if block_given?

content_tag(:div, class: "main-content-container") do
concat content_tag(:div, content, class: "main-content")
tag.div(**attributes.merge(class: ["main-content-container", attributes[:class]])) do
concat tag.div(content, class: "main-content")
concat context.sidebar if context.sidebar
end
end
Expand All @@ -15,15 +39,22 @@ def initialize(template)
@template = template
end

def sidebar(options={}, &block)
# Captures or renders the sidebar for a container block.
#
# When passed a block, the block content is captured as the sidebar content, and nil is returned.
# When no block is provided, the sidebar tag is returned (if defined).
#
# attributes - Additional HTML attributes to add to the <div> tag
def sidebar(**attributes, &block)
if block_given?
@sidebar = @template.content_tag(:aside, default_sidebar_options.merge(options), &block)
@sidebar = @template.tag.aside(**default_sidebar_options.merge(attributes), &block)
nil
else
@sidebar
end
end

protected
def default_sidebar_options
Trestle::Options.new(class: ["main-content-sidebar"])
end
Expand Down
11 changes: 11 additions & 0 deletions app/helpers/trestle/display_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
module Trestle
module DisplayHelper
# Returns a plain-text representation of the given model instance,
# typically used when rendering an associated object within a table.
#
# This helper delegates to Trestle::Display, which works by checking the
# existence of each method from `Trestle.config.display_methods` in turn
# and calling the first one it finds.
#
# By default this list is set to:
#
# [:display_name, :full_name, :name, :title, :username, :login, :email]
#
def display(instance)
Trestle::Display.new(instance).to_s
end
Expand Down
11 changes: 1 addition & 10 deletions app/helpers/trestle/flash_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module Trestle
# [Internal]
module FlashHelper
def normalize_flash_alert(flash)
flash.is_a?(Hash) ? flash.with_indifferent_access : { message: flash }
Expand All @@ -11,15 +12,5 @@ def debug_form_errors?
def instance_has_errors?
instance.errors.any? rescue false
end

def turbo_stream_update_flash
<<-EOF
<turbo-stream action="update" target="flash">
<template>
#{render_to_string(partial: "trestle/flash/flash", formats: [:html])}
</template>
</turbo-stream>
EOF
end
end
end
45 changes: 30 additions & 15 deletions app/helpers/trestle/form_helper.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,56 @@
module Trestle
module FormHelper
IDENTITY_FIELD_ERROR_PROC = Proc.new { |html_tag, instance| html_tag }
DEFAULT_FORM_CONTROLLERS = %w(keyboard-submit form-loading form-error)

def trestle_form_for(instance, options={}, &block)
# Generates a form for a resource using Rails' #form_for helper.
#
# In addition to delegating to #form_for, this helper method:
#
# 1) Sets the default form builder to `Trestle::Form::Builder`.
# 2) Sets the default :as option to match the parameter name
# expected by the admin.
# 3) Sets default Stimulus controllers on the <form> element:
# "keyboard-submit form-loading form-error"
# 4) Sets a null/identity ActionView::Base.field_error_proc as
# errors are handled by Trestle::Form::Fields::FormGroup.
# 5) Exposes the yielded form builder instance via the `form` helper.
#
# Examples
#
# <%= trestle_form_for(article, url: admin.instance_path(instance, action: :update),
# method: :patch) do %> ...
#
# Returns a HTML-safe String. Yields the form builder instance.
def trestle_form_for(instance, **options, &block)
options[:builder] ||= Form::Builder
options[:as] ||= admin.parameter_name

options[:data] ||= {}
options[:data][:controller] = (DEFAULT_FORM_CONTROLLERS + Array(options[:data][:controller])).join(" ")

form_for(instance, options) do |f|
form_for(instance, **options) do |f|
with_identity_field_error_proc do
with_form(f) { yield f }
end
end
end

# Returns the currently scoped Trestle form builder
# (a subclass of ActionView::Helpers::FormBuilder).
def form
@_trestle_form
end

protected
def with_form(form)
@_trestle_form = form
yield form if block_given?
ensure
@_trestle_form = nil
end

IDENTITY_FIELD_ERROR_PROC = Proc.new { |html_tag, instance| html_tag }

def with_identity_field_error_proc
original_field_error_proc = ::ActionView::Base.field_error_proc
::ActionView::Base.field_error_proc = IDENTITY_FIELD_ERROR_PROC
Expand All @@ -32,17 +59,5 @@ def with_identity_field_error_proc
ensure
::ActionView::Base.field_error_proc = original_field_error_proc
end

def form
@_trestle_form
end

def sidebar(&block)
content_for(:sidebar, &block)
end

def render_sidebar_as_tab?
modal_request? && content_for?(:sidebar)
end
end
end
64 changes: 47 additions & 17 deletions app/helpers/trestle/format_helper.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,60 @@
module Trestle
module FormatHelper
def format_value(value, options={})
if options.key?(:format)
format_value_from_options(value, options)
else
autoformat_value(value, options)
end
end

def format_value_from_options(value, options={})
case options[:format]
# Formats a value, either with an explicit format (given the :format option)
# or automatically based on its type using the `autoformat_value` helper.
#
# This helper is most commonly called when rendering content within a
# table cell, but is available to use from any view context.
#
# value - Value to format
# format - Symbol representing format type. Currently supported:
# :auto, nil - Auto-formats (default)
# :currency - Formats as currency using `number_to_currency`
# :tags - Formats an array of Strings as a tag list
# options - Options hash to pass to `autoformat_value` helper
#
# Examples
#
# <%= format_value(123.45, format: :currency) %>
# <%= format_value(article.tags, format: :tags) %>
#
# <%= format_value(Time.current) %>
# <%= format_value(nil, blank: "None") %>
# <%= format_value(true) %>
#
# Returns a HTML-safe String.
# Raises ArgumentError if an invalid format is given.
def format_value(value, format: :auto, **options)
case format
when :auto, nil
autoformat_value(value, **options)
when :currency
number_to_currency(value)
when :tags
tags = Array(value).map { |tag| content_tag(:span, tag, class: "tag tag-primary") }
content_tag(:div, safe_join(tags), class: "tag-list")
tags = Array(value).map { |t| tag.span(t, class: "tag tag-primary") }
tag.div(safe_join(tags), class: "tag-list")
else
value
raise ArgumentError, "unknown format: #{format}"
end
end

def autoformat_value(value, options={})
# Auto-formats a value based on its type.
#
# The current implementation of this helper supports Arrays, Time/Datetime,
# Date, true/false values, nil, String (with optional truncation) or model
# instance types (using the `display` helper).
#
# value - Value to format (multiple types supported)
# options - Options hash, usage dependent on value type. Currently supported:
# :blank - text to display for nil values (e.g. "N/A")
# :truncate - passed to truncate helper for String values
#
# Returns a HTML-safe String.
def autoformat_value(value, **options)
case value
when Array
content_tag(:ol, safe_join(value.map { |v|
content_tag(:li, v.is_a?(Array) ? v : autoformat_value(v, options)) },
tag.ol(safe_join(value.map { |v|
tag.li(v.is_a?(Array) ? v : autoformat_value(v, **options)) },
"\n"))
when Time, DateTime
timestamp(value)
Expand All @@ -37,7 +67,7 @@ def autoformat_value(value, options={})
if blank.respond_to?(:call)
instance_exec(&blank)
else
content_tag(:span, blank, class: "blank")
tag.span(blank, class: "blank")
end
when String
if value.html_safe? || options[:truncate] == false
Expand Down
Loading