coen.bakker

coen.bakker

I was trying out a pattern I haven’t tried before: sending a message from a regular API controller to a LiveView process.

See code below for some of the relevant code. In a nutshell. One route for LiveView showing a registration form. One route for handling POST requests from that form. On form submit an AJAX POST request is made from the LiveView. In case there are any validation errors the form controller tries to send a message to the LiveView. At this point that message is just a non-sense test message. The LiveView does not receive the message, however.

# Router
    live "/form", FormLive
    post "/form", FormController, :create

# PageLive
      <script>
        function handleSubmit(event) {
          event.preventDefault()
          const form = event.srcElement
          const formdata = new FormData(form)
          const ajax = new XMLHttpRequest()
          ajax.open("POST", "/form", true)
          const crsfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
          ajax.setRequestHeader('x-csrf-token', crsfToken)
          ajax.send(formdata)
        }
      </script>

      def handle_info(:hello, socket) do
        IO.puts "RECEIVED" # Does not print after controller has sent the message
        {:noreply, socket}
      end

# RouteController
        # one or several validation errors
        %{"sender_pid" => sender_pid} = changeset.params
        IO.inspect sender_pid # "#PID<0.2900.0>" Note: String
        pid = String.slice(sender_pid, 5..-2)
        pid = pid(pid) |> IO.inspect # #PID<0.2900.0>
        send(pid, :hello)
        conn
        |> Plug.Conn.resp(406, "Not all form requirements have been met.")
        |> Plug.Conn.send_resp()

I hope this code provides sufficient context for my question: It seems I can’t send messages from my controller to the LiveView, why is that? I reckon I can use PubSub to get the job done, but I prefer to understand why the pattern I’m trying doesn’t work. I looked around in docs etc., but haven’t found a definite answer, yet.

Showing Posts 1 to 10

al2o3cr

al2o3cr

What does Process.info(pid) return?

Can you show where sender_pid is captured? I’m not sure what other processes it could be picking up (a Cowboy acceptor maybe?) but it would explain what’s happening.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

As a design question, can you elaborate on why you’re doing it this way instead of submitting the form into the live view process itself? This seems like a much more complicated way of achieving phx-submit.

trisolaran

trisolaran

I would also question the design choice
. But, putting this aside, where does your controller get the pid of the LV process from? Your code seems to suggest that the pid is sent as part of the form submit. If so, how does it work? It could be you’re just sending the message to the wrong pid.

coen.bakker

coen.bakker OP

The idea is this. LiveView form for registering users. Because it is a user register, and I need to set cookies, I must use a regular controller, not a LiveView, when submitting the form. However, I use a phx-change on the form to check for validation errors as the user is filling out the form.

When the user submits an invalid form, the controller handles it. Plug.Conn requires a response being sent back to the client, but I don’t want the page to reload, because it isn’t necessary. The current page just needs to be send some info about the errors. So I don’t render() or redirect() inside the controller in case of validation errors and I try to send a message to the LiveView process.

I have made some adjustments to the code. I was sending the pid as a string as part of the form data before. The controller then reconstructed a pid from that string. That was just a temporary solution. Now the LiveView registers self() under a name and the controller uses that name to get the right pid-address to send the message to.

Message to LiveView still does not get handled by handle_info.

Here the current code with returns of IO.inspect in comments. So this is the actual code, so names are different from original post.

# LiveView
defmodule TodayForumWeb.RegistrationLive do
  use TodayForumWeb, :live_view
  import Ecto.Changeset

  alias TodayForumWeb.Router.Helpers, as: Routes
  alias TodayForumWeb.Endpoint
  alias TodayForum.Accounts
  alias TodayForum.Accounts.User

  def mount(_params, _session, socket) do
    case Process.whereis(:temp_name) do
      nil -> nil
      _ -> Process.unregister(:temp_name)
    end

    Process.register(self(), :temp_name)

    changeset = Accounts.change_user_registration(%User{})

    password_unmet_requirements =
      validations(changeset)
      |> filter_password_validations()
      |> filter_validation_texts()

    socket =
      assign(socket,
        changeset: changeset,
        password_unmet_requirements: password_unmet_requirements
      )

    {:ok, socket}
  end

  def handle_event("validate_password", %{"user" => attrs}, socket) do
    changeset =
      %User{}
      |> Accounts.change_user_registration(attrs)

    password_unmet_requirements =
      changeset.errors
      |> filter_password_errors()
      |> filter_error_texts()

    socket =
      assign(socket,
        changeset: changeset,
        password_unmet_requirements: password_unmet_requirements
      )

    {:noreply, socket}
  end

  def handle_info({:form_error, payload}, socket) do
    IO.puts "FORM_ERROR RECEIVED"
    IO.inspect(payload)
    {:noreply, socket}
  end

  def handle_info(_, socket) do
    IO.puts "SOME MESSAGE RECEIVED"
    {:noreply, socket}
  end

  def render(assigns) do
    ~H"""
    <h1>Register</h1>

    <.form
      let={f}
      for={@changeset}
      class="auth-form"
      novalidate
      phx-change="validate_password"
      onsubmit="handleSubmit(event)"
    >

      <script>
        function handleSubmit(event) {
          event.preventDefault()
          const form = event.srcElement
          const formdata = new FormData(form)
          const ajax = new XMLHttpRequest()
          ajax.open("POST", "/users/register", true)
          const crsfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
          ajax.setRequestHeader('x-csrf-token', crsfToken)
          ajax.send(formdata)
        }
      </script>

      <%= if @changeset.action do %>
        <div class="alert alert-danger">
          <p>Oops, something went wrong! Make sure your registration meets all the requirements listed below.</p>
        </div>
      <% end %>

      <%= hidden_input f, :sender_pid, value: inspect(self()) %>

      <div class="form-segment">
        <%= label f, :email %>
        <%= email_input f, :email, required: true %>
      </div>

      <div class="form-segment">
        <%= label f, :username %>
        <%= text_input f, :username, required: true %>
      </div>

      <div class="form-segment">
        <%= label f, :password %>
        <%= password_input f, :password, required: true %>
          <ul class="field-requirements-list">
            <%= for r <- @password_unmet_requirements do %>
              <li class="requirement"><%= r %></li>
            <% end %>
          </ul>
      </div>

      <div class="form-segment">
        <%= submit "Register", disabled: !Enum.empty?(@password_unmet_requirements) %>
      </div>

      <div>
        <%= link "Log in", to: Routes.user_session_path(Endpoint, :new) %> |
        <%= link "Forgot your password?", to: Routes.user_reset_password_path(Endpoint, :new) %>
      </div>
    </.form>
    """
  end

  defp filter_password_errors(errors) do
    Enum.filter(errors, fn
      {:password, _} -> true
      _ -> false
    end)
  end

  defp filter_error_texts(errors) do
    Enum.map(errors, fn
      {:password, {error_text, _validation_type}} -> error_text
    end)
  end

  defp filter_password_validations(validations) do
    validations[:password]
  end

  defp filter_validation_texts(validations) do
    Enum.map(validations, fn
      {_validation_type, validation_text} -> validation_text
    end)
  end
end

# controller
defmodule TodayForumWeb.UserRegistrationController do
  use TodayForumWeb, :controller
  import Ecto.Changeset
  alias TodayForumWeb.Router.Helpers, as: Routes
  alias TodayForum.Accounts
  alias TodayForum.Accounts.User
  alias TodayForumWeb.UserAuth

  def new(conn, _params) do
    changeset = Accounts.change_user_registration(%User{})

    password_unmet_requirements =
      validations(changeset)
      |> filter_password_validations()
      |> filter_validation_texts()

    render(
      conn,
      "new.html",
      changeset: changeset,
      password_unmet_requirements: password_unmet_requirements
    )
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.register_user(user_params) do
      {:ok, user} ->
        {:ok, _} =
          Accounts.deliver_user_confirmation_instructions(
            user,
            &Routes.user_confirmation_url(conn, :edit, &1)
          )

        conn
        |> put_flash(:info, "User created successfully.")
        |> UserAuth.log_in_user(user)

      {:error, %Ecto.Changeset{} = changeset} ->
        # password_unmet_requirements =
        #   changeset.errors
        #   |> filter_password_errors()
        #   |> filter_error_texts()

        pid = Process.whereis(:temp_name) |> IO.inspect() # #PID<0.6034.5>
        Process.info(pid) |> IO.inspect()
            # [
            #   registered_name: :temp_name,
            #   current_function: {:gen_server, :loop, 7},
            #   initial_call: {:proc_lib, :init_p, 5},
            #   status: :waiting,
            #   message_queue_len: 0,
            #   links: [#PID<0.462.0>],
            #   dictionary: [
            #     plug_unmasked_csrf_token: "L2mOf8IyDhWN41YAycnyHOck",
            #     "$initial_call": {Phoenix.LiveView.Channel, :init, 1},
            #     plug_csrf_token_per_host: %{
            #       secret_key_base: "WroRbuq4WzDg4O6RIVUsSxTUSvU9ybVwVq3X/brMP6ajlCcZVx86yZgmPx95mJ+1"
            #     },
            #     "$ancestors": [#PID<0.462.0>, #PID<0.454.0>, TodayForumWeb.Endpoint,
            #     TodayForum.Supervisor, #PID<0.406.0>],
            #     "$callers": [#PID<0.6029.5>]
            #   ],
            #   trap_exit: false,
            #   error_handler: :error_handler,
            #   priority: :normal,
            #   group_leader: #PID<0.405.0>,
            #   total_heap_size: 1597,
            #   heap_size: 987,
            #   stack_size: 12,
            #   reductions: 1318555,
            #   garbage_collection: [
            #     max_heap_size: %{error_logger: true, kill: true, size: 0},
            #     min_bin_vheap_size: 46422,
            #     min_heap_size: 233,
            #     fullsweep_after: 65535,
            #     minor_gcs: 0
            #   ],
            #   suspending: []
            # ]

        Process.alive?(pid) |> IO.inspect() # true

        payload = "the payload"
        send(pid, {:form_error, payload}) # never is received

        conn
        |> Plug.Conn.resp(406, "not all form requirements have been met")
        |> Plug.Conn.send_resp()
    end
  end

  defp filter_password_errors(errors) do
    Enum.filter(errors, fn
      {:password, _} -> true
      _ -> false
    end)
  end

  defp filter_error_texts(errors) do
    Enum.map(errors, fn
      {:password, {error_text, _validation_type}} -> error_text
    end)
  end

  defp filter_password_validations(validations) do
    validations[:password]
  end

  defp filter_validation_texts(validations) do
    Enum.map(validations, fn
      {_validation_type, validation_text} -> validation_text
    end)
  end

end

# user schema/struct
defmodule TodayForum.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "users" do
    field :email, :string
    field :first_name, :string
    field :surname, :string
    field :username, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :naive_datetime
    has_many :posts, TodayForum.Posts.Post

    timestamps()
  end

  @doc """
  A user changeset for registration.

  It is important to validate the length of both email and password.
  Otherwise databases may truncate the email without warnings, which
  could lead to unpredictable or insecure behaviour. Long passwords may
  also be very expensive to hash for certain algorithms.

  ## Options

    * `:hash_password` - Hashes the password so it can be stored securely
      in the database and ensures the password field is cleared to prevent
      leaks in the logs. If password hashing is not needed and clearing the
      password field is not desired (like when using this changeset for
      validations on a LiveView form), this option can be set to `false`.
      Defaults to `true`.
  """
  def registration_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:email, :password, :username])
    |> cast_assoc(:posts)
    |> validate_required(:username)
    |> validate_email()
    |> validate_password(opts)
    |> insert_requirement_statements()
  end

  defp validate_email(changeset) do
    changeset
    |> validate_required([:email])
    |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "Must have the @ sign and no spaces.")
    |> validate_length(:email, max: 160)
    |> unsafe_validate_unique(:email, TodayForum.Repo)
    |> unique_constraint(:email)
  end

  # password requirements
  @password_length "Use min 12 and max 72 characters."
  @password_lower_case "Add at least one lower case letter."
  @password_upper_case "Add at least one upper case letter."
  @password_special_or_number "Add at least one special or numberical character."

  defp validate_password(changeset, opts) do
    changeset
    |> validate_required([:password])
    |> validate_length(:password, min: 12, max: 72, message: @password_length)
    |> validate_format(:password, ~r/[a-z]/, message: @password_lower_case)
    |> validate_format(:password, ~r/[A-Z]/, message: @password_upper_case)
    |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: @password_special_or_number)
    |> maybe_hash_password(opts)
  end

  defp insert_requirement_statements(changeset) do
    validations = traverse_validations(changeset, fn
      # Password requirements only
      {:length, [min: _, max: _, message: _]} ->
        {:length, @password_length}

      {:format, ~r/[a-z]/} ->
        {:format, @password_lower_case}

      {:format, ~r/[A-Z]/} ->
        {:format, @password_upper_case}

      {:format, ~r/[!?@#$%^&*_0-9]/} ->
        {:format, @password_special_or_number}

      {other, opts} ->
        {other, inspect(opts)}
    end)

    %{changeset | validations: validations}
  end

  defp maybe_hash_password(changeset, opts) do
    hash_password? = Keyword.get(opts, :hash_password, true)
    password = get_change(changeset, :password)

    if hash_password? && password && changeset.valid? do
      changeset
      # If using Bcrypt, then further validate it is at most 72 bytes long
      |> validate_length(:password, max: 72, count: :bytes)
      |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
      |> delete_change(:password)
    else
      changeset
    end
  end

  @doc """
  A user changeset for changing the email.

  It requires the email to change otherwise an error is added.
  """
  def email_changeset(user, attrs) do
    user
    |> cast(attrs, [:email])
    |> validate_email()
    |> case do
      %{changes: %{email: _}} = changeset -> changeset
      %{} = changeset -> add_error(changeset, :email, "did not change")
    end
  end

  @doc """
  A user changeset for changing the password.

  ## Options

    * `:hash_password` - Hashes the password so it can be stored securely
      in the database and ensures the password field is cleared to prevent
      leaks in the logs. If password hashing is not needed and clearing the
      password field is not desired (like when using this changeset for
      validations on a LiveView form), this option can be set to `false`.
      Defaults to `true`.
  """
  def password_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:password])
    |> validate_confirmation(:password, message: "does not match password")
    |> validate_password(opts)
  end

  @doc """
  Confirms the account by setting `confirmed_at`.
  """
  def confirm_changeset(user) do
    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
    change(user, confirmed_at: now)
  end

  @doc """
  Verifies the password.

  If there is no user or the user doesn't have a password, we call
  `Bcrypt.no_user_verify/0` to avoid timing attacks.
  """
  def valid_password?(%TodayForum.Accounts.User{hashed_password: hashed_password}, password)
      when is_binary(hashed_password) and byte_size(password) > 0 do
    Bcrypt.verify_pass(password, hashed_password)
  end

  def valid_password?(_, _) do
    Bcrypt.no_user_verify()
    false
  end

  @doc """
  Validates the current password otherwise adds an error to the changeset.
  """
  def validate_current_password(changeset, password) do
    if valid_password?(changeset.data, password) do
      changeset
    else
      add_error(changeset, :current_password, "is not valid")
    end
  end
end

So this is the behaviour.

Sign up

cmo

cmo

Is it really that big of a deal to do a redirect after registering? Don’t you want to update the session and change the page, or are you planning to keep them on the registration page once they’ve registered? Or are you doing it just in case there is an error and you want to display that in the liveview? If that’s the case, why don’t you register the user in the liveview, display the error in case of error or redirect if successful.

LostKobrakai

LostKobrakai

I’d suggest taking a look at trigger-action. It allows LV form handling to trigger a POST request once the form is validated, up until that everything is handled as normal using just the LV.

coen.bakker

coen.bakker OP

Well, it isn’t a big deal to have the page reload. I sometimes go out of my way to try something new/different, to be able to learn from the experience. If there is a reason messages cannot, or should not, be sent from a controller to a LiveView, I prefer to know why.

derek-zhou

derek-zhou

You can send message from anywhere to anywhere. Most likely you didn’t have the right pid. No one knows the pid except the LV process itself and its supervisor. How did you get the info to the controller?

coen.bakker

coen.bakker OP

If registration was successful, the user is redirected (or that’s the plan at least). If registration is not successful, they stay in the LiveView. I could rerender the LiveView if registration failed and pass the changeset returned by the failed registration. That’s how I handled it before trying to send a message from the controller to the LiveView instead.

Just use a LiveView for the registration form? I was told that the registration page can be rendered as a LiveView, but that the registration cannot be handled by a LiveView. Since LiveViews cannot set the auth token cookies.

coen.bakker

coen.bakker OP

Thank you! That’s good to know.

Must have overlooked something then. I could have sworn it is the same PID, but then I know it makes sense to keep debugging.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews