Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The navigation/client-state story #136

Closed
josevalim opened this issue Mar 28, 2019 · 3 comments
Closed

The navigation/client-state story #136

josevalim opened this issue Mar 28, 2019 · 3 comments

Comments

@josevalim
Copy link
Member

josevalim commented Mar 28, 2019

This issue describes the steps we need to take to improve the navigation story with LiveView. For example, if you have a LiveView with three tabs and as, the user changes tabs, you want to update the browser URL. Or, if you are navigating a table, you want to paginate it and annotate where in the table you are.

To implement this, the main function will be Socket.push_path(socket, path). Once push_path/2 is called, the current path is compared to the one currently stored in the socket and, if they are different, the new path is sent to the client. Once the path arrives in the client, it is pushed to the stack with window.pushState.

A sample use of push_path/2 would look like this:

def handle_event("paginate", %{page: page}, socket) do
  case Integer.parse(page) do
    {int, ""} -> assign(socket, :page, page) |> fetch()
    _ -> {:ok, socket}
  end
end

defp fetch(socket) do
  data = Repo.all(build_query_from_assigns(socket.assigns))
  path = Router.table_path(socket, Map.take(socket.assigns, [:page, :order]))
  socket |> assign(:data, data) |> push_path(path)
end

Note we may use a better name than push_path, as that can be confused in itself with a router helper. Similarly, we should also add a replace_path/2, that triggers replaceState in the client side.

On the client side, once the state is popped, LiveView will detect that, set the whole LiveView container to loading and send an appropriate event to the server. The event will send the host + path. On the server, LiveView will match the path against the router, extract the parameters, and call mount again.

To implement this, we need to:

  1. When we first build the LiveView (in disconnected mode), we will need to store the router in the LiveView session.
  2. In order to match the route on LiveView, we will invoke the router.__match_route__/4 with a manually built connection. The desired parameters will be under path_parameters (this means we don't need to put path_parameters in the internal session anymore, we can always rebuild them)
  3. We will call mount again. However, connected? needs to return false, which is counter intuitive. I recommend renaming connected? to something else.
  4. Since path_parameters is becoming an essential part of LiveView, it probably makes sense to move them out of the session and make them an explicit argument to mount. We can probably name them mount(session, params, socket)
@josevalim
Copy link
Member Author

josevalim commented Mar 29, 2019

An update: unfortunately the URL approach does not work with nested live views. We will probably have to restrict this to somehow.

EDIT: for now, we can limit push_path and replace_path to the root. If you need to change it from a child view, you can send the root a message. For this purpose, we should add root_pid to the socket.

@hubertlepicki
Copy link

If we change URL in the browser using JavaScript/LiveView, we probably also want to handle back and forward browser buttons, i.e. implement the reverse: changing URL on the client would trigger switching between tabs.

Another thing a programmer has to take care here is making sure that the application renders the same page on given path of what he or she just set from LiveView, so that when user reloads the page, they would see the same thing they are looking at.

And when we start capturing "back" and "forward" button press events, we would capture them all and then somehow need to make distinction was it a nomral navigation and we should carry on and make full page reload, or maybe was it user changing URLs within the same root LiveView... That's sort of two sources of truth to me too, as both LiveView and Phoenix Router decides what should be rendered on what URL.

I wonder if we need all that trouble, maybe what we need is something like Turbolinks - or even Turbolinks itself to handle transitions between pages?

In your example, @josevalim tabs would have individual URLs, mapping to individual routes in Router, possibly initializing the same root LiveView with a different data representing different tab. When user clicks on other tab, full page gets reloaded by stealth, DOMs merged and user sees new tab.

@josevalim
Copy link
Member Author

Very good points @hubertlepicki. I had a conversation with @chrismccord today and we have a new proposal that I submitted on #174 that (I believe) handles the concerns you brought up.

That's sort of two sources of truth to me too, as both LiveView and Phoenix Router decides what should be rendered on what URL.

Just a tiny comment: in the approach proposed here, we would tie those two together so there is no duplication. We also do it in the new approach.

Looking forward to the feedback on the new proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants