Phoenix_live_view: live_render/3 after post request breaks live_redirect/2 leex template links

Created on 21 Feb 2020  路  8Comments  路  Source: phoenixframework/phoenix_live_view

Environment

  • Elixir version (elixir -v):

    • Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

    • Elixir 1.10.1 (compiled with Erlang/OTP 21)

  • Phoenix version (mix deps): * phoenix 1.4.13 (Hex package) (mix)
  • Phoenix LiveView version (mix deps): phoenix_live_view 0.7.1 (Hex package) (mix)
  • NodeJS version (node -v): v13.7.0
  • NPM version (npm -v): 6.13.6
  • Operating system: Linux 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64 GNU/Linux
  • Browsers you attempted to reproduce this bug on (the more the merrier):

    • Mozilla Firefox 73.0.1

    • Chromium 79.0.3945.130 built on Debian 10.2, running on Debian 10.3

  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no:

Actual behavior

  1. In a live view form, submit a POST request that triggers a live_render/3 return from a controller
  2. All live_redirect/2 templates in the user facing html are no longer clickable, throwing the following JavaScript console error when clicked:
TypeError: this.main is null app.js line 119 > eval:568:19
    value webpack:///../deps/phoenix_live_view/priv/static/phoenix_live_view.js?:568
    value webpack:///../deps/phoenix_live_view/priv/static/phoenix_live_view.js?:861
    value webpack:///../deps/phoenix_live_view/priv/static/phoenix_live_view.js?:836
// phoenix_live_view.js; lines 562-581
{
        key: "replaceMain",
        value: function (e, t) {
          var n = this,
              i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null,
              r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : this.setPendingLink(e),
              o = this.main.el, // ERROR here; line 568:19
              a = this.main.id;
          this.destroyAllViews(), this.main.showLoader(this.loaderTimeout), U.fetchPage(e, function (c, u) {
            if (200 !== c) return U.redirect(e);
            var s = document.createElement("template");
            s.innerHTML = u;
            var l = s.content.childNodes[0];
            if (!l || !n.isPhxView(l)) return U.redirect(e);
            n.joinView(l, null, e, t, function (e) {
              n.commitPendingLink(r) ? (n.destroyViewById(a), o.replaceWith(e.el), n.main = e, n.main.showLoader(), i && i()) : e.destroy();
            });
          });
        }
      }

Expected behavior

The live_redirect/2 links should render new pages when clicked. No javascript error should be thrown.

Most helpful comment

We will not allow live_redirects from DeadViews because live navigation and valid URLs cannot be guaranteed. You should use live routes and avoid rendering LiveViews from the controller

All 8 comments

live_redirect links are only supported via LiveViews rendered from the router via live definitions. I will try to improve the error in such cases, but this is not expected to work. Thanks!

Would it be possible to have the live_redirect links fallback to normal anchor tags when an exception occurs? This is the behaviour when users have JavaScript disabled.

live_redirect links indeed fallback to regular links when JS is disabled :)

We will not allow live_redirects from DeadViews because live navigation and valid URLs cannot be guaranteed. You should use live routes and avoid rendering LiveViews from the controller

live_redirect links indeed fallback to regular links when JS is disabled :)

This surprised me when I discovered it! I am very thankful for the hard work and careful design. :smile:

We will not allow live_redirects from DeadViews because live navigation and valid URLs cannot be guaranteed. You should use live routes and avoid rendering LiveViews from the controller

I have difficulty understanding how the URL is no longer guaranteed when a no JS fallback exists.

Without this fallback, I will need to rethink how I manage live view -> session mutations in my app.

Can you provide some insight into my thought process? (based off of https://github.com/phoenixframework/phoenix_live_view/issues/501#issuecomment-558286950)

  1. LiveView inserts a value in the DB indicating a session change is necessary
  2. LiveView calls redirect/2 to a non live route for checking DB pending session updates
  3. Non live controller conditionally modifies connection session (put_session/3)
  4. Controller deletes the session change value from the DB
  5. Controller calls redirect/2 back to the LiveView

Am I missing something obvious? This seems like a cumbersome approach.
Is there a better way without incurring 2 DB operations (insert, delete) for modifying browser session state?

We can support the regular link fallback, but I am saying there is no good reason to use live_render from a controller or regular view other than slotting LV into an existing app. You lose features, usually have redundant data access, code paths, etc. If you have a controller which does a put_session, it can redirect back to live route instead of rendering the LV.

The reason I was using live_render/3 was to pass a changeset into the session (through opts).

The redirect approach does not have this parameter, and the changeset can be too large to use conn assign/3.

Edit: clarifying problem (writing this on mobile)

Back at my laptop. Here is a more concrete example of motivation for the live_render/3 fallback.
I want to send the changeset from the post controller to the liveview.
It is unclear how to do this using Phoenix.Controller redirect/2.

# defmodule MyAppWeb.Router

# stub router code snippet
scope "/auth" do
  live "/signin", AuthLive.SignIn, :new
  post "/signin", AuthController, :post_signin
end
# defmodule MyAppWeb.AuthController

# stub post controller function
def post_signin(conn, %{"session" => session}) do
  changeset = %User{}
    |> User.changeset(session)
    |> Map.put(:action, :insert)

  conn
    |> put_flash(:error, "Invalid username or password")
    # current approach, but breaks links
    # |> live_render(MyAppWeb.AuthLive.SignIn, session: %{"changeset" => changeset})

    # values from assign cannot retrieved within liveview socket
    |> assign(:changeset, changeset)

    # Plug.Conn.CookieOverflowError:
    # cookie named "_myapp_key" exceeds maximum size of 4096 bytes
    |> put_session(:changeset, changeset) 

    # Links work, but how to pass the changeset?
    |> redirect(to: Routes.auth_sign_in_path(conn, :new))
end
# MyAppWeb.AuthLive.SignIn

# stub live view mount function, how to retrieve the changeset?
def mount(_params, %{"changeset" => changeset} = session, socket) do
  {:ok, socket
    |> assign(changeset: changeset)
end
Was this page helpful?
0 / 5 - 0 ratings