Как установить контекст просмотра в Rails?

Я создаю драгоценный камень, который перехватывает поведение рендеринга Rails по умолчанию. Вместо визуализации представления он ищет вспомогательный метод, соответствующий имени действия текущего контроллера; затем он использует возвращаемое значение вспомогательного метода для создания ответа HTML.

Например, когда вызывается действие show объекта ProductsController, драгоценный камень вызовет метод show объекта ProductsHelper. Затем он преобразует возвращаемое значение этого метода в HTML и отображает его.

Вот соответствующий код из моего гема. Он работает внутри контекста контроллера:

def render_helper_method action_name, options = {}
  helper_module = "#{self.class.name.gsub('Controller', '')}Helper".constantize

  if helper_module.instance_methods(false).include?(action_name.to_sym)
    content = helper_module.instance_method(action_name).bind(view_context).call
    original_render({ html: Hiccdown::to_html(content).html_safe, layout: true }.merge(options))
  else
    original_render({ action: action_name }.merge(options))
  end
end

Проблема возникает в первой строке условного оператора.

Как видите, я привязываю вспомогательный метод к view_context. Опять же, этот код выполняется в контроллере, так что это view_context контроллера. Однако вид view_context у них другой (я сравнил их object_id). Я понимаю, что это ожидаемая разница в любом приложении Rails — видимо, контроллер и представление не должны быть одинаковыми view_context. (Я убедился, что мой драгоценный камень не вносит такой разницы.)

Хотя контроллеры и представления должны иметь разные view_context, хелперы и представления должны иметь одинаковые view_context. Им требуется один и тот же контекст, чтобы иметь общее состояние, необходимое для поддержки таких функций, как content_for.

Поэтому я считаю, что вспомогательный метод должен быть привязан к view_context, который в конечном итоге будет использоваться для рендеринга страницы, макета приложения и всего остального. (Полагаю, это что-то вроде проблемы с курицей и яйцом, поскольку мне нужна ссылка на этот контекст, прежде чем я позвоню render!)

Я ищу способ привязать вспомогательный метод к правильному view_context. Я также открыт для достижения той же функциональности другим способом, который вообще не вызывает этой проблемы.

🤔 А знаете ли вы, что...
Rails активно поддерживается большим сообществом разработчиков, что обеспечивает наличие обширной документации и готовых решений.


1
109
1

Ответ:

Решено

Я вижу два решения: создать компонент или сделать обработчик шаблона.

Компонент

Этот компонент получит экземпляр view_context при рендеринге:

class Hiccdown::Component
  def initialize(helper_module, action_name)
    @helper_module = helper_module
    @action_name = action_name
  end

  def render_in(view_context)
    Hiccdown.to_html(
      @helper_module.instance_method(@action_name).bind_call(view_context)
    )
  end

  def format
    :html
  end
end

Обновите метод рендеринга:

def render_helper_method action_name, options = {}
  helper_module = "#{self.class.name.gsub('Controller', '')}Helper".constantize

  if helper_module.method_defined? action_name
    original_render(Hiccdown::Component.new(helper_module, action_name), options)
  else
    original_render({ action: action_name }.merge(options))
  end
end
# app/helpers/home_helper.rb

module HomeHelper
  def show
    # this should work now
    content_for :header do
      "header"
    end
    [:p, "content"]
  end
end

https://guides.rubyonrails.org/layouts_and_rendering.html#rendering-objects


Обработчик шаблонов

Поскольку вы создаете язык шаблонов, было бы неплохо иметь:

# config/initializers/hiccdown.rb

# render hiccdown: [:p, "content"]
ActiveSupport.on_load(:action_controller) do
  ActionController::Renderers.add :hiccdown do |source, options|
    render html: Hiccdown.to_html(source).html_safe, layout: options[:layout]
  end
end

# render .hiccdown templates
class HiccdownHandler
  def self.call(template, source)
    %{ Hiccdown.to_html(#{source}.compact) }
  end
end
ActiveSupport.on_load(:action_view) do
  ActionView::Template.register_template_handler :hiccdown, HiccdownHandler
end

Пример:

class HomeController < ApplicationController
  def show
    render hiccdown: [:h1, "title"]
  end
end

или создайте шаблон:

# app/views/home/show.html.hiccdown

[
  content_for(:title, "foo"),
  [:h1, "title"]
]