tl;dr below that asks the question
intercooler.js lets one replace or append parts of a page with HTML fragments that come from the back end.
Right now, I create a whole new request that returns the full page and target the specific fragment that needs to be updated.
I’d rather return only the fragment that needs to be updated. I already have my templates set up in fragments.
For example, here is a rough sketch of what I have for an recurrence rule picker:
When a different freq
is picked, we need to render a slightly different form #{frequency_selection}.html
. (i.e. daily.html
, weekly.html
, monthly.html
, or yearly.html
)
<div class="form-group row" id="<%= input_id(@form, :rrule) %>">
<%= label(@form, :rrule, "Interval", class: "col-md-1 col-form-label text-right") %>
<%# a nasty little hack to add params to fields that don't exist in the changeset %>
<% virtual_form = virtual_form(@form, :rrule) %>
<%# we get the frequency_selection POSTed from the conn or "monthly" if nil %>
<% frequency_selection = get_current_frequency_selection(@conn) || "monthly" %>
<%= select(virtual_form,
:freq,
[{"Daily", "daily"},
{"Weekly", "weekly"},
{"Monthly", "monthly"},
{"Yearly", "yearly"}],
selected: frequency_selection,
class: "col form-control",
style: "max-width: 7em;",
"ic-post-to": Phoenix.Controller.current_path(@conn),
"ic-select-from-response": "#frequency_selection",
"ic-target": "#frequency_selection") %>
<div class="col form-group row" id="frequency_selection">
<%= render("#{frequency_selection}.html", form: virtual_form, on_selection: "day") %>
</div>
</div>
Right now, I just re-render this whole template. Really, I only need to render the #{frequency_selection}.html
and leave everything else out.
When I change the frequency_selection
, Intercooler will POST
with params like this:
%{
"_csrf_token" => "JywYcQgZajkEVQo5BwsoDipdKAMnJgAAUa36FSSuTbDZFjZyM0CkIQ==",
"_method" => "POST",
"_utf8" => "✓",
"ic-current-url" => "/invoices/new",
"ic-element-id" => "invoice_specification_rrule_freq",
"ic-element-name" => "invoice_specification[rrule][freq]",
"ic-id" => "1",
"ic-request" => "true",
"ic-select-from-response" => "#frequency_selection",
"ic-target-id" => "frequency_selection",
"ic-trigger-id" => "invoice_specification_rrule_freq",
"ic-trigger-name" => "invoice_specification[rrule][freq]",
"invoice_specification" => %{
"account_id" => "",
"rrule" => %{"freq" => "yearly"},
"terms" => %{"late_fee" => "", "net" => ""}
}
}
At first, I tried using something that @OvermindDL1 created with a slight modification:
defmodule Intercooler.Plug do
import Plug.Conn
import Phoenix.Controller
require Logger
def init(options), do: options
def call(conn, _options) do
case conn.params["ic-request"] do
"true" ->
Logger.debug(fn -> "start of intercooler request" end)
view = view_module(conn)
target = Intercooler.get_target_id(conn)
conn
|> put_layout({BillingWeb.LayoutView, "intercooler.html"})
|> put_layout_formats(["html"])
|> put_format("html")
|> render(view, target, conn: conn)
_ ->
conn
end
end
end
I plug this in the :browser
pipeline in my router. What I want is to be able to request the same URL (Phoenix.Controller.current_path(@conn)
) so that the View module associated with that controller will be called. BUT I want to bypass the controller actions and just go straight to rendering from the View module.
In the View module, I would have something like this:
def render("frequency_selection", assigns) do
assigns = do_something_with_assigns(assigns)
selection = get_selection(assigns)
render("#{selection}.html", assigns)
end
When plugged in the browser pipeline, I get something like:
** (exit) an exception was raised:
** (KeyError) key :phoenix_view not found in: %{BillingWeb.Router => {[], %{}}, :phoenix_endpoint => BillingWeb.Endpoint, :phoenix_flash => %{}, :phoenix_format => "html", :phoenix_pipelines => [:browser], :phoenix_router => BillingWeb.Router, :plug_session => %{"_csrf_token" => "rM+GNJ9LP7NcAarwgmkhnw=="}, :plug_session_fetch => :done}
Understandable - I assume the controller is what’s responsible for selecting the View module.
OK, let me try in the actual controller:
** (RuntimeError) cannot render template "frequency_selection" without format. Use an atom if the template format is meant to be set dynamically based on the request format
I think I’m getting the hang of this. I am using the wrong render function. Here’s the last attempt I’ve made:
In the controller, I override action/2
:
def action(conn, params) do
case conn.params["ic-request"] do
"true" -> Intercooler.render(conn, params)
_ -> apply(__MODULE__, action_name(conn), [conn, params])
end
end
And have this render/2
function in the Intercooler
module:
def render(conn, _params) do
view_module = Phoenix.Controller.view_module(conn)
target = get_target_id(conn)
{:safe, data} = Phoenix.View.render_to_iodata(view_module, target, conn: conn)
Phoenix.Controller.html(conn, data)
end
It works, but it feels like I’m fighting Phoenix to get this to happen.
tl;dr
Is there a better way to:
- Make a request on the same path (no special intercooler controller or path, just the same path as where the original fragment was rendered)
- Identify a request with
%{"ic-request" => "true"}
- Route it to the appropriate Controller
- So that I can render the fragment from the View associated with that controller
?