Current section

26 Versions

Jump to

Compare versions

72 files changed
+383 additions
-345 deletions
  @@ -1,249 +1,17 @@
1 1 # Hologram
2 2
3 - Hologram is a full-stack isomorphic Elixir web framework that can be used on top of Phoenix.
3 + Build rich, interactive UIs entirely in Elixir using Hologram's declarative component system. Your client-side code is intelligently transpiled to JavaScript, providing modern frontend capabilities without relying on any JavaScript frameworks.
4 4
5 - ## Inspired by
5 + Website: https://hologram.page/
6 6
7 - Hologram was inspired by Elm, Phoenix LiveView, Surface, Svelte, Vue.js and Ruby on Rails.
7 + ## Sponsors
8 8
9 - ## How it works
9 + ### Early Adopters (monthly tier)
10 + * Calancea Daniel, [@D4no0](https://github.com/D4no0)
11 + * Lucas Sifoni, [@Lucassifoni](https://github.com/Lucassifoni)
10 12
11 - Hologram simplifies web app development by breaking it down into basic building blocks: Pages and Components.
13 + If you find Hologram useful and would like to support its development, consider becoming a sponsor! Your contributions help improve the project and keep it alive.
12 14
13 - Hologram analyzes the content of your Pages, which need to follow certain conventions. Based on this analysis, it decides which code should run on the client (in the web browser) and which should run on the server. Then, it converts the client-side code into JavaScript.
15 + [Become a Sponsor](https://github.com/sponsors/bartblast)
14 16
15 - Keeping the state (data) on the client side makes the programming model simpler. With stateless or stateful components, the app becomes more scalable.
16 -
17 - In Hologram, code meant for the client is organized into "actions," while code meant for the server is organized into "commands." Actions can trigger commands, and vice versa. Both actions and commands can be directly triggered by user interactions with the web page.
18 -
19 - For communication between the client and server, Hologram uses WebSockets. No additional boilerplate code is necessary - Hologram handles the setup automatically.
20 -
21 - ## I want to see some code!
22 -
23 - To understand the structure of a Hologram app and view some actual code, take a look at the feature tests app: [hologram/test/features](https://github.com/bartblast/hologram/tree/master/test/features)
24 -
25 - ## Basic example
26 -
27 - ```elixir
28 - defmodule MyPage do
29 - use Hologram.Page
30 -
31 - route "/my-page-path"
32 -
33 - def init(_params, component, _server) do
34 - put_state(component, :count, 0)
35 - end
36 -
37 - def template do
38 - ~H"""
39 - <div>Count is {@count}</div>
40 - <button $click={:increment, by: 3}>Increment by</button>
41 - <Link to={MyOtherPage}>Go to other page</Link>
42 - """
43 - end
44 -
45 - def action(:increment, params, component) do
46 - put_state(component, :count, component.state.count + params.by)
47 - end
48 -
49 - def command(:save_record, params, server) do
50 - # Do something on the server, e.g. save a record to the database
51 - end
52 - end
53 - ```
54 -
55 - ## Selling Points
56 -
57 - * State stays on the client - solving various problems as outlined below:
58 -
59 - * No latency issues arise since most of the code runs immediately on the client. This enables the creation of rich UIs or even games. Currently, with solutions that keep the state on the server (like LiveView), creating rich UIs usually requires multiple servers (in different locations) close to the users. However, latency persists, and response time cannot be guaranteed due to inherent variability. Additionally, some JavaScript or Alpine JS is still necessary to implement more advanced UI functionality. Until someone devises a solution like quantum internet (utilizing entanglement), there are no workarounds for this issue. I'm not sure if this is even technically feasible, though :wink:
60 -
61 - * Enhanced offline support, addressing scenarios such as internet disconnection or poor signal. With the bulk of code executed on the client, Hologram functions offline for extended periods. This facilitates the development of Progressive Web Apps (PWAs) or mobile apps via WebView, particularly when incorporating mechanisms like LocalStorage.
62 -
63 - * Reduced server RAM usage as the state resides in the browser.
64 -
65 - * Lower CPU utilization since the browser handles most code execution, alleviating server workload.
66 -
67 - * Decreased bandwidth consumption, as only commands necessitate communication with the server, eliminating the need to transmit component diffs for re-rendering.
68 -
69 - * Elimination of state synchronization issues, with the state centralized in the browser and WebSocket communication remaining stateless.
70 -
71 - * Minimal reliance on JS except for interfacing with select third-party scripts or widgets. This can be mitigated through standardized libraries tailored to popular packages, streamlining interoperability.
72 -
73 - * Particularly welcoming to new Elixir converts or novice developers, prioritizing DX and intuitive usability to streamline feature development without excessive technical troubleshooting or writing boilerplate code.
74 -
75 - ## Features
76 -
77 - Please note that the "Readme" file is currently undergoing an overhaul, and the "Features" section may not be up to date.
78 -
79 - ### Template Engine
80 -
81 - #### UI
82 -
83 - | Feature | Status |
84 - | :--------- | :----------------: |
85 - | Layouts | :white_check_mark: |
86 - | Navigation | :white_check_mark: |
87 - | Routing | :white_check_mark: |
88 -
89 - #### Markup
90 -
91 - | Markup | Status |
92 - | :--------------------------- | :----------------: |
93 - | Component (node) | :white_check_mark: |
94 - | Element (node) | :white_check_mark: |
95 - | Interpolation | :white_check_mark: |
96 - | For Block (iteration) | :white_check_mark: |
97 - | If Block (conditional) | :white_check_mark: |
98 - | Public Comment | :white_check_mark: |
99 - | Raw Block (markup disabling) | :white_check_mark: |
100 - | Text (node) | :white_check_mark: |
101 -
102 - ### Elixir Syntax
103 -
104 - #### Data Types
105 -
106 - | Type | Status |
107 - | :-------- | :----------------: |
108 - | Atom | :white_check_mark: |
109 - | Bitstring | :white_check_mark: |
110 - | Float | :white_check_mark: |
111 - | Function | :white_check_mark: |
112 - | Integer | :white_check_mark: |
113 - | List | :white_check_mark: |
114 - | Map | :white_check_mark: |
115 - | PID | :white_check_mark: |
116 - | Tuple | :white_check_mark: |
117 -
118 - #### Control Flow
119 -
120 - | Expression | Status |
121 - | :--------- | :----------------: |
122 - | Case | :white_check_mark: |
123 - | Cond | :white_check_mark: |
124 - | If | :white_check_mark: |
125 - | Unless | :white_check_mark: |
126 - | With | :x: |
127 -
128 - #### Function Calls
129 -
130 - | Function Type | Status |
131 - | :----------------- | :----------------: |
132 - | Anonymous Function | :white_check_mark: |
133 - | Function Capture | :white_check_mark: |
134 - | Local Function | :white_check_mark: |
135 - | Remote Function | :white_check_mark: |
136 -
137 - #### Operators
138 -
139 - ##### Overridable General Operators
140 -
141 - | Operator | Status |
142 - | :------- | :----------------: |
143 - | unary + | :white_check_mark: |
144 - | unary - | :white_check_mark: |
145 - | + | :white_check_mark: |
146 - | - | :white_check_mark: |
147 - | * | :white_check_mark: |
148 - | / | :white_check_mark: |
149 - | ++ | :white_check_mark: |
150 - | -- | :white_check_mark: |
151 - | and | :white_check_mark: |
152 - | && | :white_check_mark: |
153 - | or | :white_check_mark: |
154 - | \|\| | :white_check_mark: |
155 - | not | :white_check_mark: |
156 - | ! | :white_check_mark: |
157 - | in | :white_check_mark: |
158 - | not in | :white_check_mark: |
159 - | @ | :white_check_mark: |
160 - | .. | :white_check_mark: |
161 - | ..// | :white_check_mark: |
162 - | <> | :white_check_mark: |
163 - | \|> | :white_check_mark: |
164 - | =~ | :x: |
165 - | ** | :x: |
166 -
167 - ##### Special Form Operators
168 -
169 - | Operator | Status |
170 - | :------- | :----------------: |
171 - | ^ | :white_check_mark: |
172 - | . | :white_check_mark: |
173 - | = | :white_check_mark: |
174 - | & | :white_check_mark: |
175 - | :: | :white_check_mark: |
176 -
177 - ##### Comparison Operators
178 -
179 - | Operator | Status |
180 - | :------- | :----------------: |
181 - | == | :white_check_mark: |
182 - | === | :white_check_mark: |
183 - | != | :white_check_mark: |
184 - | !== | :white_check_mark: |
185 - | < | :white_check_mark: |
186 - | > | :white_check_mark: |
187 - | <= | :white_check_mark: |
188 - | >= | :white_check_mark: |
189 -
190 - ##### Bitwise Module Operators
191 -
192 - | Operator | Status |
193 - | :------- | :----------------: |
194 - | &&& | :x: |
195 - | <<< | :x: |
196 - | >>> | :x: |
197 - | \|\|\| | :x: |
198 -
199 - ##### Custom and Overriden Operators
200 -
201 - | Operator | Status |
202 - | :-------- | :----------------: |
203 - | custom | :white_check_mark: |
204 - | overriden | :white_check_mark: |
205 -
206 - #### Error Handling
207 -
208 - | Keyword | Status |
209 - | :------ | :----------------: |
210 - | After | :x: |
211 - | Catch | :x: |
212 - | Else | :x: |
213 - | Raise | :x: |
214 - | Rescue | :x: |
215 - | Throw | :x: |
216 - | Try | :x: |
217 -
218 - #### Guards
219 -
220 - | Construct | Status |
221 - | :---------------------- | :----------------: |
222 - | Anonymous Function | :white_check_mark: |
223 - | Case Expression | :white_check_mark: |
224 - | Comprehension Generator | :x: |
225 - | Private Local Function | :x: |
226 - | Public Local Function | :x: |
227 -
228 - #### Other Syntax
229 -
230 - | Construct | Status |
231 - | :------------- | :----------------: |
232 - | Comprehensions | :x: |
233 -
234 - ### Misc Features
235 -
236 - | Feature | Status |
237 - | :------------------ | :----------------: |
238 - | Regular expressions | :x: |
239 -
240 - ### Framework Runtime
241 -
242 - #### Core
243 -
244 - | Feature | Status |
245 - | :------- | :----------------: |
246 - | Actions | :white_check_mark: |
247 - | Commands | :white_check_mark: |
248 - | Cookies | :x: |
249 - | Session | :x: |
\ No newline at end of file
17 + Thank you for your support!
\ No newline at end of file
  @@ -44,7 +44,7 @@ export default class CommandQueue {
44 44 return (resp) => {
45 45 CommandQueue.remove(currentItem.id);
46 46
47 - const nextAction = Interpreter.evaluateJavaScriptCode(resp);
47 + const nextAction = Interpreter.evaluateJavaScriptExpression(resp);
48 48
49 49 if (!Type.isNil(nextAction)) {
50 50 Hologram.executeAction(nextAction);
  @@ -24,7 +24,7 @@ export default class Deserializer {
24 24 }
25 25
26 26 if (value.startsWith("__function__:")) {
27 - return Interpreter.evaluateJavaScriptCode(value.slice(13));
27 + return Interpreter.evaluateJavaScriptExpression(value.slice(13));
28 28 }
29 29
30 30 if (value.startsWith("__integer__:")) {
  @@ -0,0 +1,12 @@
1 + "use strict";
2 +
3 + import Bitstring from "../../bitstring.mjs";
4 + import Interpreter from "../../interpreter.mjs";
5 +
6 + const Elixir_Hologram_JS = {
7 + "exec/1": (code) => {
8 + return Interpreter.evaluateJavaScriptCode(Bitstring.toText(code));
9 + },
10 + };
11 +
12 + export default Elixir_Hologram_JS;
  @@ -31,12 +31,19 @@ import TransitionEvent from "./events/transition_event.mjs";
31 31 import ManuallyPortedElixirCldrLocale from "./elixir/cldr/locale.mjs";
32 32 import ManuallyPortedElixirCldrValidityU from "./elixir/cldr/validity/u.mjs";
33 33 import ManuallyPortedElixirCode from "./elixir/code.mjs";
34 + import ManuallyPortedElixirHologramJS from "./elixir/hologram/js.mjs";
34 35 import ManuallyPortedElixirHologramRouterHelpers from "./elixir/hologram/router/helpers.mjs";
35 36 import ManuallyPortedElixirIO from "./elixir/io.mjs";
36 37 import ManuallyPortedElixirKernel from "./elixir/kernel.mjs";
37 38 import ManuallyPortedElixirString from "./elixir/string.mjs";
38 39
39 - import {attributesModule, eventListenersModule, init, toVNode} from "snabbdom";
40 + import {
41 + attributesModule,
42 + eventListenersModule,
43 + init,
44 + toVNode,
45 + vnode,
46 + } from "snabbdom";
40 47
41 48 const patch = init([attributesModule, eventListenersModule]);
42 49
  @@ -154,7 +161,8 @@ export default class Hologram {
154 161
155 162 Hologram.executeAction(nextAction);
156 163 } else {
157 - Hologram.render();
164 + // TODO: consider: remove when there is a proper client-side bitstring implementation in place
165 + window.requestAnimationFrame(() => Hologram.render());
158 166 }
159 167
160 168 if (!Type.isNil(nextPage)) {
  @@ -371,6 +379,13 @@ export default class Hologram {
371 379 ManuallyPortedElixirCode["ensure_compiled/1"],
372 380 );
373 381
382 + Interpreter.defineManuallyPortedFunction(
383 + "Hologram.JS",
384 + "exec/1",
385 + "public",
386 + ManuallyPortedElixirHologramJS["exec/1"],
387 + );
388 +
374 389 Interpreter.defineManuallyPortedFunction(
375 390 "Hologram.Router.Helpers",
376 391 "asset_path/1",
  @@ -674,15 +689,17 @@ export default class Hologram {
674 689
675 690 const newVirtualDocument = Vdom.from(html);
676 691
677 - Hologram.virtualDocument.data = newVirtualDocument.data;
678 -
679 - const oldBody = Hologram.virtualDocument.children.find(
680 - (child) => child.sel === "body",
692 + // First patch the root html element itself to handle its attributes
693 + Hologram.virtualDocument = patch(
694 + Hologram.virtualDocument,
695 + vnode(
696 + "html",
697 + {attrs: newVirtualDocument.data.attrs || {}},
698 + Hologram.virtualDocument.children,
699 + ),
681 700 );
682 701
683 - const newBody = newVirtualDocument.children.find(
684 - (child) => child.sel === "body",
685 - );
702 + // Then patch head and body separately to preserve JavaScript/CSS handling
686 703
687 704 const oldHead = Hologram.virtualDocument.children.find(
688 705 (child) => child.sel === "head",
  @@ -692,6 +709,14 @@ export default class Hologram {
692 709 (child) => child.sel === "head",
693 710 );
694 711
712 + const oldBody = Hologram.virtualDocument.children.find(
713 + (child) => child.sel === "body",
714 + );
715 +
716 + const newBody = newVirtualDocument.children.find(
717 + (child) => child.sel === "body",
718 + );
719 +
695 720 Hologram.virtualDocument.children = Hologram.virtualDocument.children.map(
696 721 (child) => {
697 722 switch (child.sel) {
Loading more files…