Phoenix-style LiveView

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.

7 Likes

I liked the idea some time ago at https://github.com/bcardiff/redomi and not far along I used to build https://github.com/bcardiff/crystal-ast-helper .

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)

1 Like

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?

However that may be, I like what I’m seeing.

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.

1 Like

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.

@jgaskins What do you think about the LiveEEx part of LiveView? Is it really even necessary?

BTW, I was about to start a similar effort to this myself, so I’m glad you started it!

1 Like

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. :grin:

1 Like

That’s high praise! Thank you!

I tried to make it super easy to work with, but I also realize I have an unfair amount of insight into it. :smile: 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.

1 Like

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.

2 Likes

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?

Not sure I understand the question. Are you suggesting that this project should use Morphdom instead of Preact?

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
1 Like

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 like the idea, perhaps did not look closely at the examples but how can I apply a CSS stylesheet ?

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

Trying your Live view example with latest Firefox on Linux mint 18.2 (64 bits), but things don’t work yet but no problems sofar with Chrome

The request is 
GET /live-view HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: lJeoxX4GZ7julbotfOB9JQ==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

but I am not getting a response, think it has to do with the type of request, in Firefox inspect (network) it says that the type of request is plain.

Hmm, I’m not seeing a problem on Firefox on macOS. Is this app working for you? Its code is here if you’d like to take a look.

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.