Is it possible to make a post request without altering window.location with phoenix live view core components?

Hey there,

I’m working on a simple e-commerce platform and try to implement a session based cart (this is a follow up question to my last question).

I have a ‘classic’ controller for handling the cart and altering the session, now I want to add buttons for adding products to the product’s index view.

It currently looks like this:

# index.html.heex

<:action :let={{id, _product}}>
    <.link
      href={~p"/cart/add/#{id}"}
      method="post"
    >
      Add to cart
    </.link>
</:action>

Btw: Where can I find documentation about the :action tag? :face_with_monocle: I have no idea what it does and the doc’s search isn’t helping.

The corresponding controller at /cart/add/<id> only acknowledges the action with a 200er response.

It work’s despite it navigates the browser to the url it posts to. I’d like to make this request without redirecting the browser.

Now to my question:

  1. Is it possible with the link (or any other build in-) component to fire requests without redirects? Or do I have to write js by hand? - I know it’s not much work :slight_smile:
  2. Is it not very phoneix-ish what I’m doing? I’m almost exclusively worked with django the last years and I guess my structural habits are strong. In a django project I would reach for htmx at this point probably.

Thank you very much!

Cheers,
Mark

Yeah, since you’re already using LiveView based on your last question, you can use a LiveView click event binding to communicate with the server via a lightweight websocket event message rather than a traditional stateless HTTP request.

<div phx-click="add_item" phx-value-item-id={item_id}>

def handle_event("add_item", %{"item_id" => item_id}, socket), do: ...

For a rough mental model, stateful LiveViews replace stateless controllers, websocket events via LiveView bindings replace HTTP requests via AJAX, and LiveView server side handle_event callbacks replace traditional server side controller actions.

That looks like a named slot. It’d be defined by the function component you’re using so search for slot :action or render_slot(@action) in your project.

1 Like

That looks like a named slot. It’d be defined by the function component you’re using so search for slot :action or render_slot(@action) in your project.

Ahhh! Thank you very much @codeanpeace ! :slight_smile: Found it :+1:

Yeah, since you’re already using LiveView based on your last question, you can use a LiveView click event binding to communicate with the server via a lightweight websocket event message rather than a traditional stateless HTTP request.

That’s what I initially thought, but since I need to update the cart in the session - which is (in it’s default configuration) cookie based - I need a HTTP request cycle.

So my plan looks like this:

  1. Plug initializes the cart within the session and adds an id
  2. I wrap views in a live_session and an on_mount hands on the cart from the session to the socket and
  3. subscribes to a pubsub using the id.
  4. The cart is altered through an extra request to a classic controller that changes the cart in the session and…
  5. Triggers an update of the cart component using the pubsub.

And I’m stuck at step 4 right now. Well, I’m not really stuck, I just want to figure out if I need custom client js code for this, or if I can somehow implement it with build-in tools :slight_smile:

For anyone interested:

I couldn’t figure out, if phoenix has build in components capable of the following, but I found a way to implement the requests to the shopping cart controller that feels kinda idiomatic:

I stumbled across the blog The Pug Automatic from Henrik Nyh where he describes three ways to alter the session when using live views:

  1. Using Local Storage
  2. Using an extra DB (eg. Redis)
  3. Using an extra traditional controller

As already mentioned, I wanted to follow route three.

Using the according article I figured out, how to implement a custom hook using the LiveView’s JS api by changing the project’s app.js file:

// assets/js/app.js

let Hooks = {};
Hooks.SetSession = {
  DEBOUNCE_MS: 200,

  // Called when a LiveView is mounted, if it includes an element that uses this hook.
  mounted() {
    // `this.el` is the form.
    this.el.addEventListener("click", (e) => {
      clearTimeout(this.timeout);
      this.timeout = setTimeout(() => {
        // Ajax request to update session.
        fetch(this.el.dataset.url, {
          method: "post",
          headers: {
            "x-csrf-token": csrfToken,
          },
        });
      }, this.DEBOUNCE_MS);
    });
  },
};

// Adopt to include hooks
let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: Hooks,
});

Within the template, I can now use phx-hook to connect the hook with my buttons:

<button
      phx-hook="SetSession"
      data-url={~p"/cart/add/#{id}"}
      id={id}
    >
      Add to cart
</button>
1 Like

Edit: the link to the blog entry for variant 3 is: