How to update query params in .link components

I am building an interface for some data, where clicking on a bunch of buttons will change the data’s view.

To hold the state, the standard web way, I’ve been doing phx-click events, and push_patching when i handle the event eg.

<span class="cursor-pointer"
      phx-click={
        JS.push("select_category",
          value: %{category: key, item: value},
          page_loading: true
        )
      }
      phx-page-loading
    >

I use handle_params to extract the params from the push_patch, and then re-render the data.

This has worked really well, but i’ve come to realise this is kind of breaking the web experience. Normally I can middle click on a link to open a new tab but it does nothing, because it’s not a real link.

So my plan is to use a real link eg.

<.link 
class={[
          "cursor-pointer",
          @class
        ]}
        
        phx-click={
          JS.push("select_category",
          value: %{category: key, item: value},
          page_loading: true
        )

        phx-page-loading
      >

This allows middle click to open a new page, but it doesn’t execute the JS.push (obviously!)

So my next thought is to do

<.link 
class={[
          "cursor-pointer",
          @class
        ]}
        
        navigate={
         # code goes here
        }

        phx-page-loading
      >

I know sigil_p would work in the navigate attribute, but sigil_p isn’t exactly what i want. for one, it requires me to hardcode in the path to this component, and if it’s in a component library that’s not very good!

Secondly, I don’t really want to change the link, i just want to merge some new query parameters on top of the existing ones.

I am guessing there is some sort of helper function i can use but so far I haven’t had any success.

Any ideas?

The builtin URI module should have everything you need!

1 Like

And for complex set of options you can store the validated params in assigns, allowing you to rebuild a query string by changing/adding/removing items. Then you construct links that have all the necessary state in the URL and should work even with <.link patch={~p".../...?#{@params}"}.

In this case I think you want the patch attribute (not phx-click, not navigate). One idea to avoid hard coding the path is passing it explicitly to the component/LiveView.

Sigil p is smart about handling query strings, you can pass a keyword list or map and it will properly encode the values and assemble the final URL.

1 Like

How do I find the current liveview’s url from this module? It just manipulates URIs, no?

Maybe I wasn’t clear - I have no problem storing the params on the socket.assigns, nor merging them…

Patch is a pro-tip, thanks! I definitely want to do that.

But passing in the url of the component seems a bit hacky, no? surely there is a way to get it from the component.

Oh sorry, I did misunderstand. I still do kind of… what do you mean by “get the current URL from the component” or “URL of the component”? If I understand, there is no way to get the current URL from inside a component, you must pass it in. If you are wondering how to get the current path, I have an answer here?

I think that’s one of the cases where simplicity beat purity. I was not around before VerifiedRoutes, but understood it replaced a world where you could dynamically generate routes.

I might be mistaken but I think I read in this forum how VerifiedRoutes somehow are pragmatically better than the previous iteration.

Perhaps just give it a try?

Your current path solution would do the trick, thanks for that!

I guess I just thought that this should be the most common way of storing state in a web friendly (linkable) way, and there must be a better way to do it. I was hoping for a function that creates a url with a map injected, that i can use inside the patch attribute of .link

On a socket I have a router, a view - is there some function to turn that into a path by chance?

(oh and btw i’m not having a go at verified routes, i think they are awesome if i want to link somewhere else in the application!)

Ohhhh ok I think finally get what you mean by “a link from a component.” You want something like path_for(SomeCompoenent, query_params)? Components aren’t routable so there is no way to do this. While it would be theoretically possible with controllers and LiveViews, they can be mounted multiple times at different routes so I’m not sure how that would work.

Otherwise, there is no way to get the current path from anywhere other than the conn so Passing it in is what you want to. It’s what other libraries do (FlopPhoenix for example).

If I’m still misunderstanding, it would help a lot to give some pseudocode examples of what you are hoping to find!

1 Like

Sorry no, I haven’t explained this well at all.

Okay so say I have a function component, and its job is to render a card with a link in it.

attr :new_query_params_to_merge, :map
def my_component(assigns) do
  ~H"""
  <div class="">
    <.link patch={change_params(@new_query_params_to_merge)}
  </div>
  """
end

Given a liveview with the following socket.assigns.names

@names = [“bob”, “tim”, “henry”]

and the following template

<ul>
<%= for name <- @names do %>
  <li><.my_component new_query_params_to_merge={%{selected: name}} />
<% end %>
</ul>

I’d just like to have a change_params/1 that automatically works out which liveview it’s called from and builds the appropriate url.

Say this liveview was mounted at /names?test=text, the change_params function called with %{selected: "bob"} would output the url /names?test=text&selected=bob

an alternative way to do this would be instead of a patch attribute, introducing a new one called patch_params, which took a map of params to merge in.

I’m not sure the perfect solution, but i’d just like a each way to update the url params, overriding some of them

Does this explain it better?

It seems like this is such a common scenario and requirement that i’m missing something.

Btw I don’t fully understand what you mean by passing in the conn?

Currently the expectation is that you would store the path or whole uri received in handle_params of the outer LV and supply that to the function component to build the new url.

2 Likes

Thanks for the explanation.

Is it just me who finds this suboptimal? It’s just becoming clear to me that liveview seems to intentionally lead you away from using query params to store state - particularly in links. I will have to think about it deeper, but so far it feels like a shortcoming of the platform.

While I’d agree that this the workflows around state in parameters could be improved it’s not all shortcomings. LV actually acknowledges that the URL is global state to a page and global state always means the potential for concurrent edits overwriting each other when not careful. Hence handle_params being called only on router mounted root LVs and nowhere else as well as navigational events going through those as well. I would imagine that the need to be explicit about where to navigate too might stem from similar thoughs and/or from the fact that with controllers you couldn’t navigate to “parameters” to begin with, so validations always expected an actual path to be present even before LV.

To me this sounds like a good opportunity to turn this into a feature request: phoenix/CONTRIBUTING.md at main · phoenixframework/phoenix · GitHub

1 Like

If the goal is to not needing to handle hardcoded routes in the component, I would suggest passing the action/link instead:

attr :name, :string, required: true
attr :link, :string, required: true
def my_component(assigns) do
  ~H"""
  <div class="">
    <.link {@link}>Select <%= @name %></link>
  </div>
  """
end

And then using it like this:

<ul>
  <li :for={name <- @names}>
    <.my_component name={name} link={%{patch: ~p"/path/to/this/lv?#{Map.merge(@params, %{"selected" => name})}"} />
  </li>
</ul>

This still relies on having the current params assigned in handle_params, but the component itself must not be aware of any specific routes or parameters.

def handle_params(params, _uri, socket) do
  socket
  |> assign(:params, params) # needed to build the URLs
  |> ... # assign currently active values or defaults ...
  |> then(&{:noreply, &1})
end
1 Like