How to use hooks with data generated from the server

good Afternoon, I have a live_view component with a input hidden field “attached” to a phx-hook

1–in the template:

<form id="login_form" phx-submit="check_login_credentials">
<input type="hidden" id="login_token" phx-hook="input_login_token"/> 
.
.
.
</form>

2–in the server:
there’s a function handle_event/3 that processes the username and password and send it to an external Auth API in Django using djoser REST API. and receives a token.

def handle_event("check_login_credentials", params, socket) do
login_tkn="823703847509432870598743205987" #test dummy  data
if login_tkn do
  {:noreply, push_event(socket, "saveLoginToken", %{login_token : login_tkn}
.
.
.
    end 
end 

3—in the hooks file app.js theres a hook declared who manipulates the data sent from the server

let hooks = {}

hooks.saveLogintoken ={
mounted() {
    localStorage.clear()
    this.handleEvent("saveLoginToken", ({login_tkn}) =>  localStorage.setItem("login_token", login_tkn)
  },
};

When I put a console.log(something) in after the mounted() section it prints in the console, but any code put in this.handleEvent() section is not executed, If i take the other aproach and use
this.el.addEventListener("input", e => {....} it works but not handleEvent

could anybody explain why is not working, thanks in advanced.

Hi @eddyraz! I am not sure if this is the actual issue, but in your example I noticed a typo between the event name in push_event and the name in handleEvent. Is that difference present in your actual code as well? If so, does using the same event name in both places allow the handleEvent call back to be invoked?

Thanks it was a typo, I just edited the post.

Okay, I’m 0/1 so far, let’s try again! :smiley:

Does the name of the hook object match the value given to phx-hook? In your example they are input_login_token and saveLogintoken respectively.

Since you mentioned that a console.log works in the mounted method I think this is probably not the problem either, but I’m just trying to eliminate all the “simple” stuff.

There’s one other typo that could be problematic if it’s not just erroneous transcription.

The event payload in the Elixir side has a key of login_token, but the object destructuring on the JS side’s handleEvent call is using the key login_tkn which would be undefined based on the payload coming across the socket.

2 Likes

also a type, sorry i have to be more careful when posting.

I also changed that(it was that way in the code), but still no reults. I read the Javascript interoperability in Liveview hexdocs (JavaScript interoperability — Phoenix LiveView v0.18.16), but still no clue why isn’t working.

If you are using push_event:

Phoenix Liveview adds the string phx: in front of the event name.
So "saveLoginToken" becomes "phx:saveLoginToken"

Have a look at
https://hexdocs.pm/phoenix_live_view/0.18.6/js-interop.html#executing-js-commands

with the topic: Handling server-pushed events


let liveSocket = new LiveSocket(...)
window.addEventListener(`phx:highlight`, (e) => {
  let el = document.getElementById(e.detail.id)
  if(el) {
    // logic for highlighting
  }
})

Not sure if this is the reason for your problem

1 Like

in the docs says this:

push_event(socket, event, payload)
Pushes an event to the client.

Events can be handled in two ways:

1. They can be handled on window via addEventListener. A "phx:" prefix will be added to the event name.

2. They can be handled inside a hook via handleEvent.

So I think as I am using the hook aproach it must work without the "phx:" prefix I keep looking for a reason this is not working.

The key you set here is login_token

… while the key you seem to be unpacking on via destructuring assignment here is login_tkn.

Give this a shot:
this.handleEvent("saveLoginToken", ({login_token}) => localStorage.setItem("login_token", login_token)

Correct. The "phx:" prefix is not part of the event name when using this.handleEvent() on LiveSocket hooks.

To avoid possible issues with JavaScript destructuring while debugging, you could try replacing your handleEvent with the following:

this.handleEvent("saveLoginToken", (payload) => {
  console.log("received payload", payload)
})

…other than that, it is hard to say without having more of the code in context. Would it be possible for you to copy/paste some large blocks of code directly from your project? Please remove anything you can’t share, of course! :slight_smile: but the more context the better.

I also tried this and nothing, Iĺl post the liveview tree of livecomponents and who calls who, and also the code so youĺl have more context.

I have a live a liveview named opac who dynamically renders a navbar component , then in case user is succesfully authenticated renders a searchbar component, and if not then renders a login component, and last a footer component all that as shown below in the render function of opac liveview.

@impl true
  def render(assigns) do
    ~H"""
    <div>
      <section>
        <div>
          <%= live_component(@socket, DedalosPhoenixWeb.NavbarLive) %>
        </div>

        <%= if @is_authenticated == false do %>
          <div>
            <%= live_component(@socket, DedalosPhoenixWeb.LoginLive) %>
          </div>
        <% else %>
          <div>
            <%= live_component(@socket, DedalosPhoenixWeb.SearchBarLive, id: 3) %>
          </div>
        <% end %>
        <div>
          <%= live_component(@socket, DedalosPhoenixWeb.FooterLive) %>
        </div>
      </section>
    </div>
    """
  end

in the login component render function i check the credentials of the login form in a function as stated in the form tag
<form phx-submit="check_login_credentials" id="login_form">

the check_login_form function in opac (the login_view parent ) says this:

@impl true
  def handle_event("check_login_credentials", params, socket) do
    login_token = process_token_request(params["user_name"], params["password"], socket)

    send_token_to_browser(socket,params,login_token)  

  end

and the function send_token_to_browser states that:

  @impl true 
  def send_token_to_browser(socket, params, login_token) do

    
    if login_token do
          {:noreply, push_event(socket, "save_login_token", %{payload: login_token})}
      
      socket = socket |> assign(:is_authenticated, true)    
      {:noreply, assign(socket, params: params)}
    else
      {:noreply, assign(socket, params: params)}
    end

    end

and in the /assets/js/app.js file in the hooks section I put this as suggested by mcrumm:

let hooks = {}

hooks.saveLoginToken = {
    mounted() {
    	this.handleEvent("save_login_token", ({payload}) => {
       console.log("received payload", payload
   })	



  },    
};

also in the socket declaration I stated as recommended by the docs:

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: hooks})

I hope this shed more light on my problem, I keep digging looking for clues of why this does not work. Thanks in advance.

The problem is in send_token_to_browser:

Note that push_event returns a modified socket struct- the event is not pushed to the client until you return from the LiveView callback. You are not receiving the event on the client because you are not returning the socket struct containing the event.

2 Likes

Also, double-check the hook code I shared- you need to modify the callback’s function signature, too :slight_smile:

edited

Thanks so much,

I put it this way, and it worked:

@impl true
  def handle_event("check_login_credentials", params, socket) do
    login_token = process_token_request(params["user_name"], params["password"], socket)

    send_token_to_browser(socket, params, login_token)
  end

  @impl true
  def send_token_to_browser(socket, params, login_token) do
    if login_token do
      socket = socket |> assign(:is_authenticated, true)
      {:noreply, push_event(socket, "save_login_token", %{payload: login_token})}
    else
      {:noreply, assign(socket, params: params)}
    end
  end
1 Like