I have recently begun working on developing a shiny-like web UI stack for Julia. This is very early work (maybe 10h or so) but since I’m building it on top of Genie, the low and mid-level tiers are already done, so I can focus directly on the UI layer – thus development should be reasonably fast.
I’m very interested in early feedback – about everything really, but especially about:
1 - architecture
2 - API
(@ChrisRackauckas per our chat in London, I’d appreciate your thoughts).
The objective is to provide a high-level API which allows building web pages directly from Julia (no HTML, CSS or JS). This includes, in phase 1:
- wrappers around all the standard HTML 5 elements (done)
- wrappers around the Twitter Bootstrap CSS framework. The idea here is that Twitter Bootstrap provides a powerful, responsive (grid-based) layout library, a multitude of extra UI components and powerful theming capabilities (thousands of free and commercial themes available to style anything). The wrappers should make it simple to use, so that instead of
<div class="container">we’ll just say
container(). This means there’s no need to learn the CSS classes in Bootstrap. (this phase is work in progress).
My first attempt (actually working code):
import Genie.TwitterBootstrap.Layout: container, row, col import Genie.Flax: doc, html, head, body, link, h1 import Genie.Renderer: html! import Genie.Router: route import Genie.AppServer: startup view = container(id = "main", class = "black", fluid = true) do; [ row() do; [ col() do; [ h1() do; [ "I'm the cool title" ]end ]end ]end row(data_json = "http://someurl", alignitems = "center") do; [ col() do "I'm first!" end col() do "I'm number 2!" end col() do "Nooo, I'm last :((" end ]end ]end "<div id=\"main\" class=\"black container-fluid\"><div class=\"row\"><div class=\"col\"><h1>I'm the cool title</h1></div></div><div class=\"row align-items-center\" data-json=\"http://someurl\"><div class=\"col\">I'm first!</div><div class=\"col\">I'm number 2!</div><div class=\"col\">Nooo, I'm last :((</div></div></div>" layout = html(lang = "en") do; [ head() do; [ link(rel = "stylesheet", href = "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css") ]end body() do; [ view ]end ]end "<html lang=\"en\"><head><link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css\"></head><body><div id=\"main\" class=\"black container-fluid\"><div class=\"row\"><div class=\"col\"><h1>I'm the cool title</h1></div></div><div class=\"row align-items-center\" data-json=\"http://someurl\"><div class=\"col\">I'm first!</div><div class=\"col\">I'm number 2!</div><div class=\"col\">Nooo, I'm last :((</div></div></div></body></html>" route("/") do html!(layout) end startup(8001)
This builds a responsive web page using Twitter Bootstrap:
There are a lot more layout options which are already available (per https://getbootstrap.com/docs/4.1/layout/grid/)
At the moment, invoking these functions returns HTML code as a string. I like this because it’s fast. The alternative is to get back HTMLElements objects (with properties and children and a string representation) – but I’m very concerned that holding a large structure in memory to represent a big HTML document will be very resource intensive. This is a complicated problem with all the front-end frameworks (especially when searching for nodes) leading to approaches like delayed and bulk updates, shadow-DOM, etc. So I’m quite happy with the strings, in this regard.
The issue with the strings though is that this is basically immutable data. Once you invoke a function to build an element you get back the string representation and you can’t change its properties (other than through clunky string manipulation). This can become a problem in the future when I’m planning on having more complex UI elements with two-way communication (the UI elements will update their Julia representation in the backend, to reflect user input). However, I think this can be addressed with a purely functional approach. Instead of building and manipulating objects, we can define a callback – a function which will be invoked on the backend when a certain event is triggered on the frontend.
A possible middle ground might be to provide a few helper methods which accept some HTML string, pass it into Gumbo (to turn it into a DOM structure), apply the changes and return the modified string.
At the moment, the wrappers take a variable number of keyword arguments and a function which represents the children. When more than one child, the function should return an array, hence the
[ ... ] square brackets.
I think this does a good job of representing a nested DOM structure but it’s too noisy due to the
]end garbage. Any suggestions on how to make this more beautiful and readable?
I justed extended the API a bit so now we can do:
view = container(id = "main", class = "black", fluid = true, [ row( col( h1("I'm the cool title") ) ) row(data_json = "http://someurl", alignitems = "center", [ col("I'm first!") col("I'm number 2!") col("Nooo, I'm last :((") ]) ])
Lispyyyy! Definetely cleaner so maybe we have a winner?
Also any other feedback is welcome. I’m not a heavy shiny user so I’m now learning a lot more about it (and Plotly’s Dash) – the idea is to come up with something inspired by these, but 100% Julian and obviously better