Last active
February 8, 2018 23:18
-
-
Save wkirby/612841a28720538d620c3260f6ae238d to your computer and use it in GitHub Desktop.
Boring Presenters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Optionally extend the ActiveRecord model to | |
# allow you to take an ActiveRecord instance and call | |
# `#presenter` on it to get an automatically-bound | |
# presenter. | |
# | |
# The downside to this is that it will use a separate | |
# instance of the presenter for each instance of | |
# a model, instead of sharing a single presenter | |
# that gets re-bound to each instance of a model. | |
module Boring::ActiveRecordExtension | |
extend ActiveSupport::Concern | |
def presenter(view_context:) | |
if @__presenter.nil? | |
presenter_class = #{self.class.name}Presenter".classify.safe_constantize | |
if presenter_class.present? | |
@__presenter = presenter_class.new(view_context: view_context).bind(self) | |
else | |
fail "Could not locate presenter class for #{self.class.name}." | |
end | |
end | |
@__presenter | |
end | |
end | |
# include the extension | |
ActiveRecord::Base.send(:include, Boring::ActiveRecordExtension) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Boring | |
class Presenter | |
private | |
attr_reader :view_context | |
alias_method :v, :view_context | |
@__arguments = {} | |
class << self | |
attr_accessor :__arguments | |
end | |
def self.arguments(args) | |
@__arguments = args | |
class_eval do | |
define_method(:initialize) do |view_context:, **bindings| | |
unless view_context.kind_of?(ActionView::Base) | |
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}." | |
end | |
@view_context = view_context; | |
self.class.__arguments.each do |arg_name, arg_class| # dies if nil or empty | |
arg_value = bindings[arg_name] | |
# Ensure all of our bindings are the appropriate type | |
if bindings.has_key?(arg_name) && !arg_value.kind_of?(arg_class) | |
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}." | |
end | |
instance_variable_set("@#{arg_name}", arg_value) | |
end | |
# Ensure we don't have any unexpected arguments | |
extra_bindings = (bindings.keys - args.keys) | |
raise "Unexpected argument: #{extra_bindings.join(",")}." unless extra_bindings.empty? | |
end unless method_defined?(:initialize) | |
define_method(:bind) do |**bindings| | |
self.class.__arguments.each.each do |arg_name, arg_class| | |
arg_value = bindings[arg_name] | |
unless arg_value.kind_of?(arg_class) | |
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}." | |
end | |
instance_variable_set("@#{arg_name}", arg_value) | |
end | |
end unless method_defined?(:bind) | |
private | |
attr_reader *args.keys | |
end | |
end | |
def before_each_method(*) | |
# Ensure everything is properly bound before invoking this method | |
self.class.__arguments.each do |arg_name, arg_class| | |
arg_value = send(arg_name.to_sym) | |
raise "Argument '#{arg_name}' is not bound." unless arg_name.present? | |
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}." unless arg_value.kind_of?(arg_class) | |
end | |
end | |
def self.method_added(method_name) | |
return if self == Boring::Presenter | |
return if @__last_methods_added && @__last_methods_added.include?(method_name) | |
skipped_methods = [:initialize, :v, :view_context, :bind] | |
return if skipped_methods.include?(method_name) | |
skipped_methods = @__arguments.keys | |
return if skipped_methods.include?(method_name) | |
with = :"#{method_name}_with_before_each_method" | |
without = :"#{method_name}_without_before_each_method" | |
@__last_methods_added = [method_name, with, without] | |
define_method with do |*args, &block| | |
before_each_method method_name | |
send without, *args, &block | |
end | |
alias_method without, method_name | |
alias_method method_name, with | |
@__last_methods_added = nil | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# views/users/index.html.erb | |
<ul> | |
<% @users.each do |user| %> | |
<% @user_presenter.bind(user: user) %> | |
<li> | |
<p>Full Name: <%= @user_presenter.name %></p> | |
<p>Birthday: <%= @user_presenter.birth_date %></p> | |
</li> | |
<% end %> | |
</ul> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# presenters/user_presenter.rb | |
class UserPresenter < Boring::Presenter | |
# Declare the arguments needed to bind to presenter and their type | |
arguments user: User | |
# Declare pass-through methods | |
delegate :birth_date, to: :user | |
# Methods to be handled by the presenter | |
def name | |
[user.first_name, user.last_name].reject(&:blank?).join(" ") | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# controllers/users_controller.rb | |
class UsersController < ApplicationController | |
def index | |
@users = User.all | |
@user_presenter = UsersPresenter.new(view_context: view_context) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment