phoenixframework / phoenix_live_view

Rich, real-time user experiences with server-rendered HTML
https://hex.pm/packages/phoenix_live_view
MIT License
6.11k stars 915 forks source link

Cannot read property 'getDescendentByEl' of undefined #870

Closed himrock922 closed 4 years ago

himrock922 commented 4 years ago

Environment

Actual behavior

click button on phx-click, console error.

phoenix_live_view.js:820 Uncaught TypeError: Cannot read property 'getDescendentByEl' of undefined
    at e.value (phoenix_live_view.js:820)
    at eval (phoenix_live_view.js:783)
    at W (phoenix_live_view.js:393)
    at e.value (phoenix_live_view.js:782)
    at e.value (phoenix_live_view.js:812)
    at eval (phoenix_live_view.js:991)
    at Object.debounce (phoenix_live_view.js:1269)
    at e.value (phoenix_live_view.js:1135)
    at eval (phoenix_live_view.js:990)

Expected behavior

If the user click button connected "phx-click", "handle_event" method execute.

app.js

import 'jquery/dist/jquery.slim.min';
import 'fuse.js/dist/fuse';
import 'popper.js/dist/popper.min';
import 'bootstrap/dist/js/bootstrap.min';
import 'bootstrap-select-dropdown/dist/bootstrap-select-dropdown.min';
import "phoenix_html"

import NProgress from "nprogress"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"

$(window).on('load',function(){
  if(document.URL.match("daily_reports/new")) {
    let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
    let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

    // Show progress bar on live navigation and form submits
    window.addEventListener("phx:page-loading-start", info => NProgress.start())
    window.addEventListener("phx:page-loading-stop", info => NProgress.done())

    // connect if there are any LiveViews on the page
    liveSocket.connect()

    // expose liveSocket on window for web console debug logs and latency simulation:
    // >> liveSocket.enableDebug()
    // >> liveSocket.enableLatencySim(1000)
    window.liveSocket = liveSocket
  }
});

new.html.leex

<button phx-click="add_articles" class="btn btn-success">#</button>

new.ex

defmodule WeReportsWeb.DailyReports.New do
    use WeReportsWeb, :live_view
    use Phoenix.LiveView, layout: {WeReportsWeb.LayoutView, "live.html"}
    alias WeReports.UserManager
    require IEx

    @impl true
    def mount(_params, session, socket) do
      {:ok, user} = WeReportsWeb.Live.AuthHelper.load_user(session)
      group_lists = get_user_groups(user.id)
      socket = assign(socket, :current_user, user)
      socket = assign(socket, :groups, group_lists.groups)
      {:ok, assign(socket, query: "", results: %{})}
    end

    @impl true
    def handle_event("add_articles", _value, socket) do
      IEx.pry
    end

ref Issue

https://github.com/phoenixframework/phoenix_live_view/issues/748

chrismccord commented 4 years ago

I cannot recreate this using the provided code. Are you sure you updated your js assets? If you can provide a complete project to reference or more info it would be helpful. It's also possible your IEx.pry is timing out, but things worked as expected on my side. Thanks!

himrock922 commented 4 years ago

@chrismccord

I'm sorry, I did provide some files, but load_user helper did not exists.

auth_helper.ex (ref https://github.com/ueberauth/guardian_phoenix/issues/6#issuecomment-596260232)

defmodule WeReportsWeb.Live.AuthHelper do
  @moduledoc "Helpers to assist with loading the user from the session into the socket"
  @claims %{"typ" => "access"}
  @token_key "guardian_default_token"
  def load_user(%{@token_key => token}) do
    case Guardian.decode_and_verify(WeReports.UserManager.Guardian, token, @claims) do
      {:ok, claims} ->
        WeReports.UserManager.Guardian.resource_from_claims(claims)
      _ ->
        {:error, :not_authorized}
    end
  end
  def load_user(_), do: {:error, :not_authorized}
end

this file provide user_resource by session via guardian.

First mount callback execute, socket assigns successed. But, Second mount callback execute fail.

IEx.pry(1st.)

    6:     @impl true
    7:     def mount(_params, session, socket) do
    8:       IEx.pry
    9:       case WeReportsWeb.Live.AuthHelper.load_user(session) do
   10:          {:ok, user} ->

Allow? [Yn] Y

Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> session
%{
  "_csrf_token" => "eWNCiAe5cu0lnAtp9Lkb47DH",
  "guardian_default_token" => "*****"
}

IEX.pry(2nd.)

iex(1)> continue[info] CONNECTED TO Phoenix.LiveView.Socket in 162µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "Pzk2OwcqHRhUJlszLQcgA3cCLRpgATAZZnxxnkx-7Sk_CFTsNNFxT6tQ", "vsn" => "2.0.0"}
Request to pry #PID<0.930.0> at WeReportsWeb.DailyReports.New.mount/3 (lib/we_reports_web/live/daily_reports/new.ex:8)

    6:     @impl true
    7:     def mount(_params, session, socket) do
    8:       IEx.pry
    9:       case WeReportsWeb.Live.AuthHelper.load_user(session) do
   10:          {:ok, user} ->

Allow? [Yn] Y

Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> continue
pry(1)> session
%{}
pry(2)> socket
#Phoenix.LiveView.Socket<
  assigns: %{
    flash: %{},
    live_action: nil,
    live_module: WeReportsWeb.DailyReports.New
  },
  changed: %{},
  endpoint: WeReportsWeb.Endpoint,
  id: "phx-Fg5MOkNNbRGb6wrB",
  parent_pid: nil,
  root_pid: #PID<0.930.0>,
  router: WeReportsWeb.Router,
  view: WeReportsWeb.DailyReports.New,
  ...
>

mount callback is execute twice...?

rockwood commented 4 years ago

I'm seeing this as well. My mount callback is executed twice. When phx events fire before the second mount call, I get the error Cannot read property 'getDescendentByEl' of undefined.

Router:

    scope "/sellers" do
      live "/new", SellerLive, :new
    end

LiveView:

defmodule MyApp.SellerLive do
  use MyAppWeb, :live

  def mount(_params, plug_session, socket) do
    IO.inspect "MOUNT"
    current_session = plug_session |> Map.get("my_app", %{}) |> Session.from_state()

    {:ok, assign(socket, :current_session, current_session)}
  end

  ...

Logs:

[info] GET /member/sellers/new
[debug] Processing with Phoenix.LiveView.Plug.new/2
  Parameters: %{}
  Pipelines: [:browser, :member]
"MOUNT"
[debug] QUERY OK ...
[info] Sent 200 in 33ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 246µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "LRIOCzcDKw0EOn5XY3tZMngXYSImGBA4ECmxcTOFAu6zV50y5aWjAzOg", "vsn" => "2.0.0"}
"MOUNT"
[debug] QUERY OK ...
pedrogmucino commented 4 years ago

I have exactly the same problem, apparently the file \apps\myapp web\priv\static\js\app.js is not updating causing this issue ¿Does anyone fixed the problem?

josevalim commented 4 years ago

Remove assets/node_modules, then do a cd assets; npm install and either start your server in dev or re-run your deployment scripts.

pedrogmucino commented 4 years ago

@josevalim Done it and the problem persists

ijunaid8989 commented 4 years ago

I have a form and phx_validate just get called once I load the page.

tomtaylor commented 4 years ago

We see this intermittently on our CI environment on one specific end-to-end Cypress test. It's very difficult to reproduce locally.

It's possible it's being triggered by a browser click event occurring before the connected mount. Our end-to-end tests run very rapidly, much faster than a user would be able to click.

@josevalim I can privately share video captures of it occurring in Cypress, if useful? We will continue to see if we can reproduce locally.

josevalim commented 4 years ago

Can you please try master? We have many fixes in there. Make sure you nuke node_modules after updating.

tomtaylor commented 4 years ago

Thanks @josevalim, I'm afraid it's still occurring on master. I've blown away node_modules.

I can now reproduce locally, intermittently, inside Cypress. Here's the stack trace that Cypress produces from our compiled JS.

https://gist.github.com/tomtaylor/b3a262465b8eb258647023f1c8653df3

The source map links the first line (http://localhost:4003/js/admin.js:2108:37) back to this snippet.

{
key: "getViewByEl",
        value: function (e) {
          var t = e.getAttribute("data-phx-root-id");
          return this.getRootById(t).getDescendentByEl(e); <-- this line
        }
}

I can supply the code and the source map privately if useful.

It looks like this is related to the debounce function. We're not using debounce in this view, but there's a throttle on a form, which the link that causes this is inside.

tomtaylor commented 4 years ago

I've updated the gist above to show the unobfuscated stack trace.

tomtaylor commented 4 years ago

I can see in Cypress that the difference between the failing runs and the passing runs is that there's no data-phx-root-id attribute set on the <div data-phx-main=true> element when the button is pressed. I can reproduce this much more reliably by enabling the latency simulator with a 1s delay.

Does this attribute get set once the live socket is connected? If so, it might explain why pressing the button before the socket connection is made causes this issue.

tomtaylor commented 4 years ago

I can now reliably work around this by explicitly waiting for the presence of a div with a phx-connected class before proceeding with the test.

josevalim commented 4 years ago

Oh, I see. This makes sense. In the actual application, you can't use forms and friends until phx-connected is on, because we disable the cursor and everything. But your tests were not waiting for this, which causes said errors.

tomtaylor commented 4 years ago

Yeah, I think Cypress does a bunch of trickery to trigger events as a user would, but I guess it's not respecting whatever mechanism LV uses to disable/enable these. Thanks for your help!

Bumppoman commented 4 years ago

I'm having this issue with a nested LiveView. My setup is a main page, "/foo", and several Bootstrap tabs, which can also be accessed by "/foo/bar", "/foo/baz", etc. Accessing "/foo" and clicking on the "bar" tab works fine, but directly accessing the URL "/foo/bar" causes this error.

Unfortunately, it kills JS execution on the page and makes the page unusable until refreshed. Still happening with 0.14.0.

MeterSoft commented 3 years ago

have some issue on 0.14