Commit c5804997 authored by Greg Gard's avatar Greg Gard

finishing up basic presenter/component functionality

parent 82bf8e02
class ApplicationController < ActionController::Base
before_action :set_presenter
# override in subclasses to load a different presenter by default
def self.default_presenter
Object.const_get(name.demodulize.sub(/Controller$/, 'Presenter')) # rescue ApplicationPresenter
end
# *******************************
# instance methods
#
attr_accessor :presenter
# all controllers get a presenter even if just base class
def set_presenter(klass = self.class.default_presenter)
self.presenter = klass.new(
request: request,
response: response,
session: session,
controller: self
)
end
end
# bare presenter-based controller.
# - relies on rails router to enforce get/post resource semantics although you could
# ensure this by adding .get/.post to methods below eg presenter.get(:transfers) below or
# if you were using presenter outside of router/controller.
# TODO: either a presenter included module and/or class method set_default_routes which would create this boilerplate
#
class TransfersController < ApplicationController
def index
render html: presenter.get(:transfers)
end
def new
render html: presenter.new_transfer
end
def create
render html: presenter.create_transfer
end
def show
render html: presenter.show_transfer
end
def edit
render html: presenter.edit_transfer
end
def patch
render html: presenter.update_transfer
end
def update
render html: presenter.update_transfer
end
def destroy
render html: presenter.delete_ransfer
end
end
\ No newline at end of file
require Rails.root.join('lib/gw/presenter')
#
# base class for all application presenters
# - abstracts away actual implementation of third-party lib GW::Presenter
......@@ -8,5 +7,6 @@ require Rails.root.join('lib/gw/presenter')
#
class ApplicationPresenter < GW::Presenter
end
\ No newline at end of file
#
# stateless component mixins
# - goal to create "functionally pure" methods with no external dependencies or side-effects.
# - rails' tag implementation does, under the hood, rely on an output buffer in the target class (eg presener).
# might be interesting to convert these to self-contained classes with *_ui delegation in target class maybe.
# - inspired by React components.
#
module TransfersComponent
def transfer_template(opts = {})
tag.div(class: 'test') do
tag.h4(opts[:title])
tag._text "This is just some plain <div>text</div> that isn't html safe"
tag._html "<h1>This should be big with no tags in text</h1>"
tag.div do
tag.h3 opts[:time]
tag.a(opts[:link_label], href: opts[:link_path])
end
end
end
end
\ No newline at end of file
class TransferPresenter < ApplicationPresenter
class TransfersPresenter < ApplicationPresenter
include TransfersComponent
DEFAULTS = {
title: "Emergency Transfer Form",
......@@ -7,16 +8,20 @@ class TransferPresenter < ApplicationPresenter
link_path: 'http://gardwired.com'
}.freeze
def transfers
transfer_template DEFAULTS
end
def edit_transfer
end
def update_transfer
end
def transfer_template(opts = {})
opts = DEFAULTS.merge(opts)
def delete_transfer
<<-HTML
<h4>Emergency Transfer Form</h4>
#{link_to opts[:link_label], opts[:link_path]}
HTML
end
end
\ No newline at end of file
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
......@@ -17,3 +16,10 @@ module EmergencyTransfer
# the framework and any gems in your application.
end
end
# presenters - rails doesn't seem to recursively load all of app folder
require Rails.root.join('lib/gw/presenter')
Dir[Rails.root.join('app/presenters/components/**/*.rb')].each{|f| require f}
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :transfers
end
module GW
#
# common class/instance methods
#
module Common
def self.included(klass)
klass.extend self
end
def logger
Rails.logger
end
end
end
\ No newline at end of file
require_relative 'common'
require_relative 'refinements/tag_refinements'
module GW
class Presenter
# rails integration - adds all helpers including tags/forms as well as routing and route helper methods
include ActionView::Helpers
include ActionDispatch::Routing::UrlFor
include Rails.application.routes.url_helpers
include TagRefinements
include Common
# ***********************
# instance methods
#
attr_accessor(
:request, :response, :session, :controller, :opts,
:output_buffer # see action view context for info on buffer
)
def initialize(opts = {})
self.request = opts.delete(:request)
self.response = opts.delete(:response)
self.session = opts.delete(:session)
self.controller = opts.delete(:controller)
self.opts = opts
self.output_buffer = opts[:buffer] || ActionView::OutputBuffer.new
end
# mark output as safe for render html - assumes your outputs have been appropriately escaped
def html(method, *args)
send(method, *args).html_safe
end
# don't assume we have request object
def get(method, *args)
raise "#{self.class.name}##{method}: invalid request method #{request.request_method}" if request && !request.get?
send method, *args
end
# don't assume we have request object
def post(method, *args)
raise "#{self.class.name}##{method}: invalid request method #{request.request_method}" if request && !request.post?
send method, *args
end
end
end
\ No newline at end of file
module GW
#
# - ideally, we would use refinements; however they are still lexically scoped and so don't inherit
# and don't work with dynamic method invocation (send) among other issues.
# - this isn't as elegant as what a refinement would be if it were dynamically scoped, but
# at least this isn't in scope for entire application.
#
module TagRefinements
class BufferedTagBuilder < ActionView::Helpers::TagHelper::TagBuilder
def tag_string(name, content = nil, escape_attributes: true, **options, &block)
content = @view_context.capture(self, &block) if block_given?
@view_context.output_buffer << if VOID_ELEMENTS.include?(name) && content.nil?
"<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe
else
content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes)
end
# we could return self and be able to chain, but then we would have to
# have some kind of dump command.
@view_context.output_buffer
end
# nokogiri-ish sugar
def _text(content)
@view_context.output_buffer << content
@view_context.output_buffer
end
def _html(content)
@view_context.output_buffer << content.html_safe
@view_context.output_buffer
end
end # class
# overwrite to use our custom builder class
def tag_builder
@tag_builder ||= BufferedTagBuilder.new(self)
end
end # module
end
\ No newline at end of file
require 'test_helper'
class TransferPresenterTest < ActiveSupport::TestCase
setup do
@content = TransferPresenter.new.transfer_template
@defaults = TransferPresenter::DEFAULTS
end
test "should have link" do
assert_match %r[<a.+href=>#{@defaults[:link_label]}</a>], @content, "link incorrect"
end
test "should have a title" do
assert_match %r[<h4>#{@defaults[:title]}</h4>], @content, "missing/invalid title"
end
end
require 'test_helper'
class TransfersPresenterTest < PresenterTestCase
attr_accessor :defaults
setup do
self.defaults = TransfersPresenter::DEFAULTS
self.content = TransfersPresenter.new.transfer_template(defaults)
end
test "should have a link" do
assert_select "a", defaults[:link_label]
assert_select "a:match('href', ?)", defaults[:link_path]
end
end
......@@ -8,3 +8,30 @@ class ActiveSupport::TestCase
# Add more helper methods to be used by all tests here...
end
# presenter tests allows unit testing of view components without needing request/integration tests
# - assumes component methodsa are built "functionally pure" with no side effects or dependencies (eg global request object)
# - inspired by react.
# - https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb
# - some other view test helpers http://api.rubyonrails.org/v5.2.0/classes/ActionView/TestCase/Behavior.html
#
class PresenterTestCase < ActiveSupport::TestCase
include Rails::Dom::Testing::Assertions
# - dom assertions use document_root_element as the html element to test. this should be an dom element instance
# eg Nokogiri::HTML::Document.
# - "content" is simpler and abstracts out the actual implementation if rails makes an internal api change.
# - use in setup block or in test methods: self.content = some_html_component_method
attr_accessor :document_root_element
def content=(html)
self.document_root_element = Nokogiri::HTML(html) # sugar for full form below
end
def content
document_root_element || Nokogiri::HTML::Document.new
end
end
class ComponentTestCase < PresenterTestCase
end
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment