-
Notifications
You must be signed in to change notification settings - Fork 949
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
Request for feedback: Format rendering override public API #3631
Comments
I'm not sure how this would work and would probably require major deviations in our diffing engine. Unless you're just wanting to keep the server bits json unaware and have the client treat it as raw statcis + dynamics that it zips together, then JSON parses, and computes a diff? If you're wanting streaming JSON diffs, where the server sends diff patches containing the key space and such, I don't think we can marry things up. If you're wanting some kind of .json.heex that just produces the same thing as HTML it could be made to work but there's escaping rules and a bunch of things probably that I'm not thinkingg about so I'm not sure if it makes sense. Can you explain a bit more what you're going for on the server and client? Also Have you taken a look at https://github.com/hansihe/live_data or https://hex.pm/packages/live_state yet? How do those compare to what you're after? Thanks! |
The difference between the other two projects is unifying the effort under LiveView as the state management backend. This allows for a single state and event handling framework for clients, if you already have an existing LiveView application then adding JSON rendering for specific clients is just a matter of adding the renderer. As far as the how, in this case JSON diffing won't use |
In my spike, I'm changing the defp to_rendered_content_tag(socket, tag, view, attrs) do
case Map.fetch(socket.private, :renderer) do
{:ok, func} ->
func.(socket, tag, view, attrs)
:error ->
rendered = Phoenix.LiveView.Renderer.to_rendered(socket, view)
{_, diff, _} = Diff.render(socket, rendered, Diff.new_components())
content_tag(tag, attrs, Diff.to_iodata(diff))
end
end |
Hi @bcardarella! Your spike only changes the dead render. I am afraid that, in order to change the live render, it is quite more complex as the diff engine keeps its own state (so it knows what to send to the client) and interacts with several parts of the lifecycle, especially component management. So I can think of two high-level options:
In both cases, figuring out what is part of Diff and what is part of the new API would certainly be lots of work. Diff is pretty much the heart of LiveView. |
@josevalim yes, I'm just piece-mealing as I go. So pointers in the right direction are always appreciated. |
I'm interested in your vision of how this would be used from an application point of view.
Are we talking about web applications where some pages are LiveViews, but others are managed by a frontend framework like React? Or would it be more of a full fletched SPA with LiveView as the state layer? How do you imagine things like live navigation to work? So if you can clarify a little bit how the lifecycle of such an application would look like, that would be very helpful. As an example (completely made up):
Again, this is just one of many ways I could imagine this to work and probably not the one you are thinking about. Depending on how many LiveView features are actually useful for such an application, I am wondering if starting from scratch with Phoenix Channels and a custom client would be easier? |
I don't see how starting from scratch is a viable recommendation. |
You asked for pointers in the right direction and, given we lack all context, the best we can do is to list all possibilities, including rolling your own. We don’t have enough context to rule any of them in or out. :) |
Perhaps a better way to describe this: imagine if Phoenix's Controllers were originally written to ever only handle and respond with HTML. That's the state of evolution that I see LiveView currently being at. The more real-world comparison was earlier versions of Rails. My recollection of pre-1.0 or Rails and for a period of time after that was Rails only responded to and with HTML content. But the needs moved beyond that and gave Rails additional utility that maintained its relevance but also allowed it to be used in more versatile ways. Ultimately, LiveView is transporting data from server to client. That data represents a state. Whether that state is being represented in the format of HTML, JSON, XML, SwiftUI, etc... should be an implementation detail. The possibilities of what LiveView could be as both a transformative approach to API design but bringing all of the performance, lifecycle, state management, and developer productivity benefits beyond just HTML. We've proven this out, to a degree, with LiveView Native. I'm happy to demonstrate for you all what we've been able to accomplish on that front. But even I have to admit that LVN isn't going to solve all the needs of native application development. We're simply not going to win over a ton of people outside of Elixir because to get LVN you need to accept Elixir up and down the stack, which is a huge buy-in cost. One area that I believe that Elixir has the easiest foot in the door for companies with existing tech stacks: building API backends. We've seen this at DockYard with requests to build API backends for React, SwiftUI, etc... front-ends is very common. We try to sell and convert them over to LiveView but almost every single time those efforts have failed. If we implement a REST or GraphQL backend what lock-in do they have with Elixir at that point? None. If they want to change out to another stack with REST or GraphQL they can easily migrate over. If instead there is a compelling reason to stay with Elixir not only will that increase the retention goals of the language but also I believe they'd start use of Elixir beyond their initial needs. If that API is through LiveView's programming model not only do they get a unique and, IMO, better API backend experience for both their users (lifecycle management, performance) but also through developer productivity. Now they're just one step away from just adding HEEx template to start using LiveView for template rendering as well. The issues as I see it with LiveState and LiveData is that neither of those two libraries offer a path beyond just what they offer. There's no value add beyond just the immediacy. They may fulfill the ask of this issue in that they provide JSON patching or a wireformat for data binding in the client, but then what? With LiveView as the platform for the underlying programming model we could see the use of LiveView start to metastasize within organizations. If they are using it as their backend API already, writing a few templates to represent all of the state and event handling they've already written to support their JSON rendering is so low overhead. That's my TED talk. |
Just to follow up on my analogy of Phoenix Controllers, delegating the rendering of other formats to libraries outside of LiveView I see as being the same as if Phoenix Controller stayed with just HTML and the recommendation to get JSON, XML, etc... responses were punted to other libraries outside of the Phoenix's ecosystem. |
While I agree with the sentiment, it doesn't necessarily hold after an in-depth look. For example, going back to Rails (and Phoenix), the controller actually does not care about the format, it fully delegates the rendering to another layer, which is the view/template. In Phoenix, this delegation is as simple as: we are going to call a function in a module and that's it. Therefore, in the stateless world, our transport layer is handled by Plug/Controller, which actually does not care about the format. Then, once you go into specific formats, we have layered a bunch of format specific functionality:
So while your argument is that To be clear, I don't think my view above is 100% true, but I also don't think that your view that
Per the above, the reason this comparison is flawed is precisely because Another possible interpretation is that TL;DR: I agree with the sentiment that we should enable different formats but we don't have evidence that the best way to get there is by turning LiveView inside out. The actual answer will require in-depth looks into both Phoenix.Channel and Phoenix.LiveView. FWIW, I was part of the team who worked on Rails to decouple the controller layer, template lookup and template rendering, and I have written a book about it. |
I understand what you're saying from the perspective of the "View" but if we're going to stick to that analogy of MVC purism then LiveView itself goes way beyond that. It's managing data and events as well. So I'm not looking at this through the lens of what fits into which design pattern bucket but from the perspective of the public API that people are interacting with. And that is purely the LiveView. Yes, everything is still flowing through a Controller and yes you could arge that the LiveView is just the way to represent that but reality is that there are no guides, documentation, or recommendations that anyone should be interacting with and building functionality into anything but their LiveView. If the argument here is to stick to MVC patterns as the guide then I would argue that LiveView already draws way outside these lines. However, the counter-point that we've seen in the SPA world is that separating these concerns into MVC isn't the only way to do it. In fact, the majority of this functionality is being built into a single component, which is exactly what LiveView is doing. |
I am not arguing from a MVC purist point of view. This discussion is for library authors who will be building the toolkits used by developers. From a stateless library author point of view, I have Plug and a collection of functionality that I can stick together to build libraries for HTML apps and APIs. The exact pieces I assemble will differ between formats. From a stateful library author point of view, you could have Phoenix.Channel and a collection of additional modules, such as Async, Uploads, etc, for building libraries for HTML and JSON apps. For stateless requests, the dev entry point is shared across HTML and JSON, but they already diverge in the router and the actual rendering tends to be very different (templates vs protocols). Stateful could also be very similar, where the dev entry point is the socket functionality, provided by Phoenix.Channel on both server and client, but then it diverges. Once again, looking at Phoenix.LiveView and saying "this should all be used for JSON" is similar to looking at everything Plug and Phoenix have which are specific to HTML and saying "this should be all used for JSON". None of them will hold in general. Someone has to go in and tell exactly what is used for what, where, and why. |
Just to wrap this up as I step away from the computer for the rest of the day, one of your points were:
The general direction is correct but there is nothing requiring us to use the exact same library to get there. Take a look at the generated stateless HTML and JSON in Phoenix apps today (without comments): # Uses Phoenix.Template
defmodule DemoWeb.UserHTML do
use DemoWeb, :html
embed_templates "*.html"
end # Uses nothing
defmodule DemoWeb.UserJSON do
def index(%{users: users}) do
%{users: ...}
end
end We could totally have this as the stateful version: # It could just be DemoWeb.UserHTML, I'm adding "Live" for clarity
# Use Phoenix.LiveView
defmodule DemoWeb.UserLiveHTML do
use DemoWeb, :live_html
def render(assigns) do
~H"""
...
"""
end
end # Use Phoenix.XYZ
defmodule DemoWeb.UserLiveJSON do
use DemoWeb, :live_json
def render(%{users: users}) do
%{users: ...}
end
end There's nothing forcing us to use the same libraries for them. In the same way they do not use the same ones for stateless rendering. Only the entry point is the same, which could totally be the Finally, I'd also add that the majority of front-end developers I reached out lately said that TypeScript integration is a strong requirement for those, which is yet another major departure from LiveView, as HTML templates are just strings. |
@josevalim I think we're arguing for the same things here. For example, here is what Alpha's project structure looks like: In the context of this application, the "Live" module doesn't permit a In other words, if I'm understanding what you're saying is that if we were to back out on where you feel the correct point of abstraction should happen is within |
But I can't say for sure, as I still don't understand what you are going for from a practical point of view (PS: I'm gone for the day). |
No need to reply immediately, I don't want to lose my state of mind so I'm going to provide my replies right now.
Yes, I know. The I'm not sure what the best way to present this is right now. I've given my motivation but that doesn't seem to be resonating very well. I am nearly done with an example application that shows this in action but I'm concerned the focus will be on the implementation than the actual benefit of this approach. Does the LV core team have a recommendation on how best I can go about proving the benefit here? |
I am speaking for myself but I don't think we disagree with the benefits of this direction, so there is nothing to prove there. The disagreement is on if LiveView should be the one absorbing these responsibilities (and, if so, by exposing which APIs?) or if it is better to build on top of shared abstractions, using Phoenix.Channel and the socket entry points as the Plug/Controller layer of the stateful world. |
I'm exploring the idea of producing JSON documents instead of markup and the LV diffs would be JSON Patches. I would like to get feedback on the best place to look at experimenting with this locally before I make a more concrete ask or PR with public API.
From some investigation this https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_live_view/static.ex#L286-L290 appears to be the correct place where I could override the template rendering. The idea at the moment would be to register a rendering override for a given format. If no override is detected, fallback to the existing default. This would allow API to remain intact, it doesn't change html rendering.
I'm soliciting feedback if this is the most straight forward approach or not.
The text was updated successfully, but these errors were encountered: