I saw a while back that Phoenix has a Live View module for building rich interactions for fully server-rendered apps — that is, the interactions are passed to the server, the server renders updates for the UI, and the client reconciles a vDOM for it.
I wanted to see how easy or difficult this would be in Crystal, and the result is LiveView, a shard that provides an abstract class that lets you do pretty much the same thing in a Crystal app. I’ve managed to make it framework-agnostic, so you render an instance of your subclass into your HTML to make it work. Rendering LiveView.javascript_tag into your application’s layout template wires everything up.
I’ve got a demo app running here: https://crystal-live-view-example.herokuapp.com/ It shows simple things like a click counter, true/false toggle based on checkbox state, recurring UI updates with a ticking clock, and some more intermediate examples like autocomplete and deferred data loading. The code for that example app is here.
All this happens with only 6KB in JS. Even Phoenix’s LiveView uses 29KB. I’m curious to see what you think.
Of course there are pros and cons on the approach. Some might be
pro) the programmer might be able to know less of client side things
pro) tooling like the one available in ASP.NET Webforms can be built to support RAD
cons) the app state and progress can be more fragile. The server might need to track lots of things even temporarily. The more coupled things are the less resiliant might be (exceptions in the even loop of the server vs an ajax request that didn’t work but the user can keep using the page)
This looks awesome to me, though I don’t really understand how to use it from the readme.
What does this do?: LiveView.script_tag, how would I incorporate other views?
LiveView.javascript_tag is what you put on the layout template to load the JS assets required to wire everything up. It opens a websocket connection to the server to carry the events and UI updates for views that you’ve rendered.
To render your individual views (you can define them like the examples here), you render them into your ECR templates (I imagine Slang templates would be similar). Let’s say you’ve got a view that shows the current time:
require "live_view"
class CurrentTime < LiveView
render "The current time is <time>#{Time.now}</time>"
def mount(socket)
# Set up a recurring UI update every second to ensure we always render the current time
every(1.second) { update socket }
end
end
<!doctype html>
<html>
<head>
<!-- snip -->
</head>
<body>
<!--
This might be inside a child route or something, but I'm
putting it here to have fewer examples :-D
-->
<%= CurrentTime.new %>
<%= LiveView.javascript_tag %>
</body>
</html>
You can have as many views as you like in a single HTML payload, so they’re more like UI components than a Rails-style view. The view generates an identifier that the client will use to subscribe to it. A client can subscribe to an indefinite number of views.
Currently that identifier is just a UUID. I’d like to figure out how to incorporate specifying the identifier so you can do things like update all views of a given type for a given entity. For example, if your app is for selling concert tickets, you may want to have live updates for every user looking at the page for the same concert in case they sell out before they click the “Buy” button.
Those projects look really interesting. I think we have different goals in mind, though.
I agree 100%, there are a lot of ways this can fall down. It’s common where websockets are in play to have a temporary disconnect due to a spotty internet connection, so resilient reconnects need to be supported.
Other longer-term disconnection scenarios are even more common:
user closes their laptop
user puts their phone on standby
browser puts the tab to sleep to save device battery
We don’t know when they’ll be back, so we have to put an upper limit on how long we’ll hold that UI state in memory in the app. :-) One workaround might be to refresh the page if they come back and their connected views have been GCed.
It also relies on the idea that the websocket connects to the same process that the original request connected to, so it breaks when used with the fork + reuse_port pattern. Some load balancers (including nginx) support passing a given client to the same upstream, so there are at least some decent workarounds for this.
There are a lot of ways to improve it. I’m gonna be keeping an eye on how Phoenix LiveView does some things to see how they handle some of the failure scenarios.
It might be for Phoenix.LiveView. They calculate the DOM diff on the server side to send the absolute smallest payload over the wire as possible. To accomplish that, LiveEEx is probably needed as the template engine since you need to change the format of the template output, so they use the file extension that generates that instead of the one that generates HTML.
Whether it was worth the effort for Phoenix to do it that way, I’m not sure. It’s certainly a cool optimization but it’s a lot of effort to create and maintain, so this implementation sends raw HTML and builds the Preact render tree for it on the client.
@jgaskins, I just tried the example. I must say, this is like something out of my wildest crystal shard dreams, seriously. I’ve been wanting to play with some interactive web apps written in Crystal, not JavaScript. This is it, this fits my needs perfectly. Thanks, now, unto the hacking ─ I’ve next to no experience with web apps, so the result’ll probably be bad because of me, but I’ll share it when I’ve got something.
I tried to make it super easy to work with, but I also realize I have an unfair amount of insight into it. So if you have any trouble with it, feel free to log an issue and we’ll try to figure out a good way forward.
I was intrigued with the concept of Phoenix LiveView, but it looked too big and complicated to try to do something like it in Go (my language of choice). But then I ran across your project, which seemed more approachable. I’ve made a version in Go at https://github.com/andybalholm/liveview.
Hi. I was looking for a similar solution also, and found OCaml’s Eliom framework. I liked a lot the idea, but OCaml sintax and the quality of Eliom’s website made me give up on it. The sintax, types and speed as a bonus, is that atracted my atention to Crystal. I’m looking at Elixir also as a potential language to learn, it’s a bit confusing for me bun not as much as OCaml. I’m coming from PHP world so Crystal is easier for me to understand.
Btw I’ve seen recently an iteresting project - lattice-core. Anybody know why it didnt got interest from comunity and is abandoned? It have the same idea of using websokets to interact with backend code.
Afaik on client side Phoenix LiveView. have 2 js files LiveView.js + morphdom.js.
Will it be a problem to use that code and build the same functionality in Crystal?
In fact yes. I mean, if there is already a solution for client side that works well, maybe adapting it to Crystal would be easier than writing the whole thing from scratch.
I may be wrong but I see this as 2 endpoints that communicate with messages. One dictates what to show and may process events messages. The others job is about how to show things and to send user input to server (nothing more than an advanced puts and gets in one piece)
In this case all what is needed on server side to adapt to client solution is to translate in the right way the in/out messages.
This is where I’m confused. In your posts, you’re talking about this concept as if it hasn’t been done yet, but I spent quite a bit of time in this thread talking about how I did build it, some of its limitations (most of which are constraints due to Crystal), etc.
Also, Phoenix’s LiveView implements HTML diffing on the server side to reduce the number of bytes sent over the wire for each UI update to an absolute minimum. Their client only runs the “patch” portion of DOM reconciliation — I pointed this out earlier in the thread, too.
Using their client would mean we would have to implement server-side HTML diffing in Crystal. You’re welcome to give that a shot, but I didn’t do it for 3 reasons:
that’s a lot of work I don’t want to do
there exist so many great client-side libraries for that
that uses more CPU time than we can afford
we can’t do single-process multi-core yet in Crystal, so we can’t burn that much CPU time
as it is, we need to map each WebSocket to the same server-side process that rendered the view it’s subscribing to
There are 4 parts of core functionality, not 2:
Registry that mounts/unmounts views and manages client subscriptions to them
WebSocket client that sends events to the server and manages the UI
WebSocket server to handle the messages from the client and pass them to specific view instances via the Registry
The actual views which add themselves to the Registry when rendered and send messages to WebSocket clients
I’ve looked at rich demo on Phoenix site and at your example and concluded that is not a finished product. Well, many times then browsing different Crystal tools i catch myself on the wish that documentation and examples if any is not rich enough so I can make a fast appreciation of how good is that tool. And I do browse many tools lately trying to estimate how they suit into the architecture in my mind. In fact it’s a well known problem nowadays that there are much more tools than a person have time to dig into each of them. And it’s a head ache if someone tries to make good decision then designing the stack of tech to use in future.
I do feel sorry that I pissed you, but on the other hand I’m glad that you revealed more about that is under the hood.
Yes server-side HTML diffing was one of the thing I appreciated. din’t knew about Cristal limitations. Now I know. Thank you.
I’d put the stylesheet as part of the main app that renders the app, but if you’re feeling wild you can have your LiveView render a <style> element so that your styles are completely self-contained. You might use the inline style if you’re building a view as a shard and you want the integration to be as simple as “install the shard, call the view from your app’s template”.
class RandomColorText < LiveView
render <<-HTML
<div id="my-thing">
#{@text}
</div>
<style>
#my-thing {
background-color: #{color};
}
</style>
HTML
def initialize(@text : String)
end
def color
"##{rand(0..0xffffff).to_s(16).rjust(6, '0')}"
end
end
Tried your app in Firefox (Linux mint 64 bits), and typed an address “777”, no response, No extensions are used/loaded in Firefox. Your app works in Chrome.