Skip to content

Commit

Permalink
Introduce generic renderers
Browse files Browse the repository at this point in the history
Rather than explicitly depending on partials, this allows for a more
flexible rendering process on the objects that can be rendered, by
declaring a renderer through options. These renderers can still be
partials, or component classes, or Procs that determine the renderer
dynamically.
  • Loading branch information
foca committed Feb 13, 2024
1 parent 315d94e commit b1903b3
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 70 deletions.
19 changes: 8 additions & 11 deletions lib/dtb/empty_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "has_options"
require_relative "has_i18n"
require_relative "renderable"

module DTB
# The Empty State encapsulates the data you might want to render when a query
Expand All @@ -19,10 +20,17 @@ module DTB
# behavior with it. You are welcome to use these strings in any way you want,
# or not at all.
#
# == Rendering the Empty State
#
# Each empty state is a {Renderable} object, and as such, you can define how
# to render it via the {#render_with} option, and by then calling the
# {#renderer} method.
#
# @see HasEmptyState
class EmptyState
include HasOptions
include HasI18n
include Renderable

# @!group Options

Expand All @@ -32,12 +40,6 @@ class EmptyState
# ` @see HasI18n#i18n_lookup
option :context

# @!attribute [rw] partial
# @return [String, nil] the path to a Rails partial to render for this
# empty state. By default this is +nil+ which means no partial should
# be rendered.
option :partial

# @!endgroup

# Determine the "title" of the default empty state container that is
Expand Down Expand Up @@ -70,10 +72,5 @@ def explanation
def update_filters
i18n_lookup(:update_filters, :empty_states, context: options[:context], default: "")
end

# (see #partial)
def to_partial_path
options[:partial]
end
end
end
96 changes: 70 additions & 26 deletions lib/dtb/filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "active_support/core_ext/string/inflections"
require_relative "query_builder"
require_relative "has_options"
require_relative "renderable"

module DTB
# Filters allow setting conditions on a query, which are optionally applied
Expand Down Expand Up @@ -71,24 +72,70 @@ module DTB
#
# == Rendering filters
#
# Filters accept a +partial+ option that points to the partial used to render
# them. This lets you render different widgets for each filter, where you can
# customize the form control used (i.e. a text field vs a number field vs a
# select box).
# To render a filter in the view, you can call its {#renderer} method, and
# pass the output to the +render+ helper:
#
# When rendering, the filter object is passed to the partial, same as if you
# did:
# <%= render filter.renderer %>
#
# render partial: "some_partial", locals: {filter: the_filter}
# To configure how that renderer behaves, Filters accept a +rendes_with+
# option that defines how they can be rendered. This lets you render different
# widgets for each filter, where you can customize the form control used (i.e.
# a text field vs a number field vs a select box).
#
# If you don't specify a partial for a filter, it will try to infer it from
# the filter's class name. So, for example, a +TextFilter+ will try to render
# +filters/text_filter+ while a +SelectFilter+ will try to render
# +filters/select_filter+.
# By default, filters are rendered using a partial template named after the
# filter's class. For example, a +SelectFilter+ would be rendered in the
# +"filters/select_filter"+ partial. The partial receives a local named
# +filter+ with the filter object.
#
# Alternatively, you can pass a callable to +render_with+ that returns valid
# attributes for ActionView's +render+ method. This could be a Hash (i.e. to
# +render+ a custom partial with extra options) or it could be an object that
# responds to +render_in+.
#
# Finally, you can just pass a Class. If you do, DTB will insantiate it with a
# +filter+ keyword, and return the instance. This is useful when using
# component libraries such as ViewComponent or Phlex.
#
# class SelectFilter < DTB::Filter
# option :render_with, default: SelectFilterComponent
# end
#
# == Passing extra options to the renderer
#
# Whatever options you pass to the {#renderer} method, they will be
# forwarded to the configured renderer via {#render_with}. For example,
# given:
#
# class SelectFilter < DTB::Filter
# option :render_with, default: SelectFilterComponent
# end
#
# The following two statements are equivalent
#
# <%= render filter.renderer(class: "custom-class") %>
# <%= render SelectFilterComponent.new(filter: filter, class: "custom-class") %>
#
# == Overriding the options passed to the renderer
#
# The default options passed to the rendered are the return value of the
# {#rendering_options} method. You can always override it to customize how the
# object is passed to the renderer, or to pass other options that you always
# need to include (rather than passing them on every {#renderer}) invocation.
#
# @example Overriding the rendering options
# class AutocompleteFilter < DTB::Filter
# option :render_with, default: AutocompleteFilterComponent
#
# def rendering_options
# # super here returns `{filter: self}`
# {url: autocomplete_url}.update(super)
# end
# end
#
# @see HasFilters
class Filter < QueryBuilder
include HasOptions
include Renderable

# @!group Options

Expand All @@ -107,10 +154,13 @@ class Filter < QueryBuilder
# value used as the default.
option :default

# @!attribute [rw] partial
# @return [String, nil] a custom partial to use when rendering the filter.
# Defaults to the filter's class name, underscored.
option :partial
# @!attribute [rw] render_with
# @see Renderable#render_with
option :render_with,
default: ->(filter:, **opts) {
{partial: "filters/#{filter.class.name.underscore}", locals: {filter: filter, **opts}}
},
required: true

# @!endgroup

Expand Down Expand Up @@ -155,21 +205,15 @@ def placeholder
i18n_lookup(:placeholders, default: "")
end

# Determine the partial to be used when rendering this filter. If the
# +partial+ option is set, that is used. If not, this will infer the partial
# should be based on the name of the class, such that FooFilter tries to
# render +filters/foo_filter+.
#
# @return [String]
def to_partial_path
options.fetch(:partial, "filters/#{self.class.name.underscore}")
end

# @visibility private
# @api private
def evaluate?
value.present? && super
end

private def rendering_options
{filter: self}
end

private def default_value
if options[:default].respond_to?(:call)
evaluate(with: options[:default])
Expand Down
19 changes: 9 additions & 10 deletions lib/dtb/filter_set.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require_relative "query_builder_set"
require_relative "renderable"

module DTB
# Filter sets extend {QueryBuilderSet QueryBuilder sets} by adding a few
Expand All @@ -25,6 +26,8 @@ module DTB
# <% end %>
#
class FilterSet < QueryBuilderSet
include Renderable

# @!group Options

# @!attribute [rw] param
Expand All @@ -36,10 +39,8 @@ class FilterSet < QueryBuilderSet
# @see #namespace
option :param, default: :filters, required: true

# @!attribute [rw] partial
# @return [String] the partial to use to render the filters form. Defaults
# to +"filters/filters"+.
option :partial, default: "filters/filters", required: true
# @!attribute [rw] renders_with (see Renderable#renders_with)
option :render_with, default: "filters/filters", required: true

# @!attribute [rw] submit_url
# @return [String] the URL to submit the filters form to.
Expand Down Expand Up @@ -70,18 +71,16 @@ def namespace
options[:param]
end

# @return [String] the rails partial used to render this form.
# @see #partial
def to_partial_path
options[:partial]
end

def submit_url
options[:submit_url]
end

def reset_url
options[:reset_url]
end

private def rendering_options
{filters: self}
end
end
end
6 changes: 2 additions & 4 deletions lib/dtb/has_empty_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ module DTB
#
# @example Configuring a default partial to render empty states
# class ApplicationQuery < DTB::Query
# options[:empty_state][:partial] = "data_tables/empty_state"
# options[:empty_state][:render_with] = "data_tables/empty_state"
# end
#
# @example Rendering the empty state of a data table
# <% if data_table.empty? %>
# <%= render partial: data_table.empty_state,
# as: :empty_state,
# locals: { data_table: data_table } %>
# <%= render data_table.empty_state.renderer(data_table: data_table) %>
# <% end %>
#
# @example A sample default empty state partial
Expand Down
2 changes: 1 addition & 1 deletion lib/dtb/has_filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module HasFilters
# filter :name,
# type: ContainsTextFilter
#
# @example Overriding the partial used for a specific filter
# @example Overriding the renderer used for a specific filter
# # Instead of rendering "filters/contains_text_filter", this would
# # render "example/partial" in the filters form.
# filter :name,
Expand Down
Loading

0 comments on commit b1903b3

Please sign in to comment.