Skip to content

action-view interoperability #302

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

Merged
merged 10 commits into from
Jan 29, 2020
Merged

Conversation

fiedl
Copy link
Collaborator

@fiedl fiedl commented Nov 17, 2019

Issue #302: action-view interoperability

Changes

  • The view context is exposed to matestack components and pages. (b322a69)
  • The render method, commonly used in rails, is extended to support matestack pages. (42d24fa)
  • Implicit rendering, commonly used in rails controllers, is extended to support matestack pages. (fc78b66)
  • Helper methods like link_to, which return an ActiveSupport::SafeBuffer, are implicitly wrapped in a plain component when used in matestack components or pages. (ba93b32)

Notes

The changes proposed in this pull request intend to make matestack easier to use by newcomers.

Exposing the view context

When rendering matestack pages or controllers from a controller, which includes the Matestack::Ui::Core::ApplicationHelper, the controller's view_context is passed as context to the matestack object, which exposes the view context to its components. This way, helpers and other methods exposed by the controller to the vanilla-rails views, can also be used from matestack pages or components.

For example, the current_user method from devise, the can? method from cancancan, or methods exposed by decent_exposure (bookings in the example below) can be used in matestack objects.

class BookingsController < ApplicationController
  include Matestack::Ui::Core::ApplicationHelper
  before_action :authenticate_user!
  helper_method :current_user
  expose :bookings, -> { current_user.bookings }

  def index
    responder_for Pages::Bookings
  end
end

class Pages::Bookings < Matestack::Ui::Page
  def response
    components {
      heading size: 1, text: "#{current_user.name}'s bookings"
      ul do
        bookings.each do |booking|
          li { plain booking.name }
        end
      end
      if can? :create, Booking
        link text: "Add booking"
      end
    }
  end
end

Explicit rendering of matestack pages

The render method of rails is extended to also support matestack pages. This way, the render method can be used instead of responder_for in controllers.

class Admins::BookingsController < ApplicationController
  include Matestack::Ui::Core::ApplicationHelper
  
  def index
    # The following calls are equivalent:
    render matestack: 'admins/bookings'
    render matestack: Pages::Admins::Bookings
    render Pages::Admins::Bookings
    respnder_for Pages::Admins::Bookings
  end
end

Implicit rendering of matestack pages

In rails, when a controller action has no explicit render call, the view to render is inferred from the controller name and the controller action. This pull request extends this mechanism to support matestack pages.

class Admins::BookingsController < ApplicationController
  include Matestack::Ui::Core::ApplicationHelper
  
  def index
  end
  
  def show
  end
end

class Pages::Admins::Bookings < Matestack::Ui::Page
  def response
    components {
      plain "Hello from admins/bookings#index"
    }
  end
end

class Pages::Admins::Booking < Matestack::Ui::Page
  def response
    components {
      plain "Hello from admins/bookings#show"
    }
  end
end

The index action looks for a matestack page matching the controller namespace (Admins) and the controller name in plural (Bookings). The show action looks for a matestack page matching the controller namespace and the controller name in singular (Booking).

Implicitly rendering legacy helpers in plain components

Using vanilla-rails helpers like link_to in matestack components or pages, does not raise an error, but does not render the helpers' output. For example:

class Components::Layout::SignInSignOutButton < Matestack::Ui::StaticComponent
  def response
    components {
      if not current_user
        link_to "Sign in", new_user_session_path
      else
        plain "Welcome, #{current_user.name}!"
        link_to "Sign out", destroy_user_session_path, method: :delete
      end
    }
  end
end

One could render these vanilla-rails helpers by prefixing them with a plain component (plain link_to "Sign in").

This pull request prefixes link_to and other helpers returning an ActiveSupport::SafeBuffer implicitly with plain, making the above example work.

def response
  components {
    heading size: 1, text: "The following calls are equivalent:"
    plain link_to "Sign in", new_user_session_path
    link_to "Sign in", new_user_session_path
  }
end

such that one can leave out the call to `render` or `responder_for`  in the controller alltogether.
@fiedl
Copy link
Collaborator Author

fiedl commented Nov 20, 2019

Scenario: render several static pages with the same controller action

If you find yourself in the situation where you need to render several static pages

/about
/contact
/imprint

and you don't gather information in your corresponding controller, you could just treat the name of the page ("about", "contact", "imprint") as :id in the router and pass it to a generic StaticPagesController.

Router

# config/routes.rb
Rails.application.routes.draw do
  # ...
  resources :static_pages, path: ''
end

In particular, the above resources includes this route definition for the GET request:

get ':id', to: 'static_pages#show'

Controller

# app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  # GET /:id
  #
  # For example:
  # GET /about
  # GET /imprint
  def show
    render matestack: "static_pages/#{params[:id]}"
  end

end

We still need to check the security implications here, because we are passing a parameter into a string, which is used for object creation later. For example, constrain the allowed pages right in the router:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  STATIC_PAGES = %w(about contact imprint)
  resources :static_pages, path: ''constraints: lambda { |request|
    request[:id].in? STATIC_PAGES
  }
end

Matestack Pages

# app/matestack/pages/static_pages/about
class Pages::StaticPages::About < Matestack::Ui::Page

  def response
    components {
      plain "Hello world from https://example.com/about"
    }
  end

end

# In this example, the `clients/bookings#step1` action will render
# `Pages::Clients::Bookings::Step1`.
#
def default_render(*args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this approach too much. "index" and "show" should not be translated to the plural or singular of the controller name. all actions should be treated equally like: "Pages::Clients::Bookings::Step1", "Pages::Clients::Bookings::Index", "Pages::Clients::Bookings::Index"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fiedl fiedl requested a review from jonasjabari January 28, 2020 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants