Hologram
Hologram is a full-stack isomorphic Elixir web framework that can be used on top of Phoenix.
Inspired by
Hologram was inspired by Elm, Phoenix LiveView, Surface, Svelte, Vue.js, Mint, Ruby on Rails.
How it works
The Hologram concept is that your web app is composed of the basic Hologram blocks of Layouts, Pages and Components.
Hologram builds a call graph from the content of your Pages (which must follow some basic conventions) and determines what code is to be used on the client and what code is to be used on the server. Hologram then transpiles the code to be used on the client to JavaScript.
Because the state is kept on the client, the programming model is simpler and thanks to stateless or stateful components the app is easily scalable.
Code that is to be run on the client is encapsulated in “actions”, and code that is to be run on the server is encapsulated in “commands”. Actions can trigger commands, commands can trigger actions. Both actions and commands can be triggered directly by DOM events.
The Client communicates with the Server using WebSockets. There is no boilerplate code required, Hologram automatically works out what is required.
I want to see some code!
To see how a Hologram app is structured, and see some actual code, take a look at the Hologram’s test app: hologram/test/e2e
Basic example
defmodule MyPage do
use Hologram.Page
route "/my-page-path"
def init(_params, _conn) do
%{
count: 0
}
end
def template do
~H"""
<div>Count is {@count}</div>
<button on:click={:increment, by: 3}>Increment by</button>
<Link to={MyOtherPage}>Go to other page</Link>
"""
end
def action(:increment, params, state) do
put(state, :count, state.count + params.by)
end
def command(:save_to_db, _params) do
# Repo.update (…)
:counter_saved
end
end
Is it used in production?
Yes, it's used in Segmetric: https://www.segmetric.com/. Take a look at the “join beta” and “contact pages" which showcase form handling, or check the mobile menu. This all works on transpiled Elixir code! (But please, submit the forms only if you actually want to join the Segmetric beta or contact Segmetric - it is a live page, thanks!)
Selling Points
-
State on the client - and all of the problems that get solved by this approach (below)…
-
No latency issues as most of the code is run immediately on the client. This makes it possible to create rich UI or even games. At the moment with LiveView you need something like fly.io to make it bearable, but you still have latency and can’t guarantee the response time (there is always some variance). And you still need some JS or Alpine to make it work. Until someone manages to create quantum internet (e.g. by taking advantage of entanglement), there are no workarounds for this problem. Not sure if this is even technically possible, though 😉
-
Better offline support (internet connection loss, poor signal, etc.). Since most of the code is run on the client and you only hit the server to run some commands from time to time, Hologram can work offline most of the time. This would also make it possible to create PWAs or mobile apps through WebView, assuming you use something like LocalStorage.
-
Less server RAM is used - the state is kept in the browser instead of the socket.
-
Less CPU used - most of the code is run by the browser, not by the server.
-
Less bandwidth used - only commands need to communicate with the server, no need to send diffs to rerender components.
-
No state sync problems - the state is kept only in one place (browser) and the WebSockets communication used is stateless.
-
No JS or Alpine.js is needed except for communication with some third-party scripts or widgets, but this can also be solved by creating some standardized libs for popular packages that would handle the interop.
-
Very friendly to new Elixir converts or beginner devs. I want it to be very, very intuitive so that you can focus on working on new features in your project instead of solving technical problems and writing boilerplate code.
History / background
I tried to write this kind of framework first in Ruby and managed to create a working prototype, but the performance was not satisfactory. Then I tried Crystal, but it was very hard to work with its AST. Then I moved to Kotlin, but I realized that it’s better to use a dynamically typed language … Then I found Elixir in 2018 and fell in love with it. I started work on Hologram in the summer of 2020.
Roadmap
This is a work in progress (although usable and used in production). To check what works and what is planned - take a look at the roadmap in the readme at Github bartblast/hologram](https://github.com/bartblast/hologram#readme)
To meet the objective of being a very friendly developer experience, Hologram will provide out-of-the-box such things as UI component library (CSS framework agnostic), authentication, authorization, easy debugging (with time travel), caching, localization and some other features that you typically use in a web app.
I believe that using Hologram’s approach, i.e. Elixir-JS transpilation, code on client and action/command architecture it will be possible to create something as productive as Rails, without its shortcomings related to scalability, efficiency, etc.
✅ == DONE
🚧 == IN PROGRESS (partially done, some features work)
❌ == TODO
Runtime
Core
Feature | Status | Comments |
---|---|---|
Actions | ✅ | |
Commands | ✅ | |
Routing | 🚧 | done: paths without params, todo: params |
Session | ❌ |
Template Engine
Feature | Status | Comments |
---|---|---|
Components | 🚧 | done: stateless/stateful components, todo: props DSL |
If Block | 🚧 | done: element nodes, todo: component nodes |
Interpolation | ✅ | |
Layouts | ✅ | |
Navigation | ✅ | |
Pages | ✅ | |
Raw Block | ❌ | |
Templates | 🚧 | done: template in module, todo: template in separate file |
Events
Event | Status | Comments |
---|---|---|
Blur | ✅ | |
Change | 🚧 | done: form tags, todo: input, select, textarea tags |
Click | 🚧 | done: event handling, todo: event metadata |
Click Away | ❌ | |
Focus | ❌ | |
Key Down | ❌ | |
Key Press | ❌ | |
Key Up | ❌ | |
Mouse Move | ❌ | |
Params | ❌ | |
Pointer Down | 🚧 | done: event handling, todo: event metadata |
Pointer Up | 🚧 | done: event handling, todo: event metadata |
Resize | ❌ | |
Scroll | ❌ | |
Select | ❌ | |
Submit | ✅ | |
Tap | ❌ | |
Target | ❌ | |
Touch Cancel | ❌ | |
Touch End | ❌ | |
Touch Move | ❌ | |
Touch Start | ❌ | |
Transition End | ✅ |
Tools
Tool | Status | Comments |
---|---|---|
Authentication | ❌ | |
Authorization | ❌ | |
Caching | ❌ | |
Code Reload | 🚧 | done: recompiling, reloading, todo: incremental compilation |
Localization | ❌ | |
Time Travel | ❌ |
Elixir Syntax
Types
Type | Status | Comments |
---|---|---|
Anonymous Function | 🚧 | done: regular syntax, todo: shorthand syntax, multi-clause |
Atom | ✅ | |
Binary | ❌ | |
Bitstring | ❌ | |
Boolean | ✅ | |
Exception | ❌ | |
Float | ✅ | |
Integer | ✅ | |
IO Data | ❌ | |
List | ✅ | |
Map | ✅ | |
Nil | ✅ | |
Range | ❌ | |
Regex | ❌ | |
String | ✅ | |
Struct | ✅ | |
Tuple | ✅ |
Operators
Overridable General Operators
Operator | Status |
---|---|
unary + | ✅ |
unary - | ✅ |
+ | ✅ |
- | ✅ |
* | ✅ |
/ | ✅ |
++ | ✅ |
-- | ✅ |
and | ❌ |
&& | ✅ |
or | ❌ |
|| | ✅ |
not | ❌ |
! | ✅ |
in | ✅ |
not in | ❌ |
@ | ✅ |
.. | ❌ |
<> | ❌ |
|> | ❌ |
=~ | ❌ |
Non-Overridable General Operators
Operator | Status |
---|---|
^ | ❌ |
. | ✅ |
= | ✅ |
& | ❌ |
:: | ❌ |
[ | ] | ✅ |
Comparison Operators
Operator | Status |
---|---|
== | ✅ |
=== | ❌ |
!= | ✅ |
!== | ❌ |
< | ✅ |
> | ❌ |
<= | ❌ |
>= | ❌ |
Bitwise Module Operators
Operator | Status |
---|---|
&&& | ❌ |
^^^ | ❌ |
<<< | ❌ |
>>> | ❌ |
||| | ❌ |
~~~ | ❌ |
Pattern Matching
Type | Status |
---|---|
Anonymous Function | ❌ |
Binary | ❌ |
Bitstring | ❌ |
Case | ✅ |
Comprehension | ✅ |
Cons Operator | ✅ |
Module Function | ✅ |
If | ❌ |
List | ✅ |
Map | ✅ |
Range | ❌ |
Struct | ❌ |
Tuple | ✅ |
Control Flow
Structure | Status | Comments |
---|---|---|
After | ❌ | |
Anonymous Function Call | 🚧 | done: regular syntax, todo: shorthand syntax (capture operator) |
Case | ✅ | |
Catch | ❌ | |
Comprehension | 🚧 | done: generator, todo: filter, into |
Cond | ❌ | |
Else (If) | ✅ | |
Else (Rescue) | ❌ | |
Guards | ❌ | |
If | ✅ | |
Module Function Call | ❌ | |
Raise | ❌ | |
Rescue | ❌ | |
Throw | ❌ | |
Unless | ❌ | |
With | ❌ |
Definitions
Structure | Status |
---|---|
Exception | ❌ |
Function Head | ❌ |
Macro | ❌ |
Module | ✅ |
Module Attribute | ✅ |
Private Function | ✅ |
Public Function | ✅ |
Directives
Directive | Status |
---|---|
Alias | ✅ |
Import | ✅ |
Multi-Alias | ❌ |
Require | ✅ |
Use | ✅ |
Sigils
Sigil | Status |
---|---|
~c | ❌ |
~C | ❌ |
~D | ❌ |
~N | ❌ |
~r | ❌ |
~R | ❌ |
~s | ❌ |
~S | ❌ |
~T | ❌ |
~U | ❌ |
~w | ❌ |
~W | ❌ |
Other
Feature | Status |
---|---|
Behaviours | ❌ |
Codepoints | ❌ |
Custom Sigils | ❌ |
Default Arguments | ❌ |
Function Capturing | ❌ |
Map Update Syntax | ❌ |
Module Attribute Accumulation | ❌ |
Module __info__/1 callback | ✅ |
Module Nesting | ❌ |
Protocols | ❌ |
Variable rebinding | ❌ |
Someday/Maybe
- Two-Way Binding (template engine)
Not on Roadmap
- Types: PID, Port, Reference
- Control Flow: Exit, Receive
- Operators: Custom, Overriding
- Other: Erlang Libraries