-
Notifications
You must be signed in to change notification settings - Fork 65
Hoplon Overview
- Introduction
- Components
- Components with cells
- TODO Advanced Topics
- TODO Routing
- TODO do! and on! multimethods
- TODO Lenses
- TODO Appendix A: Comparison of Components and Cells to React
- TODO Appendix B: CQRS
- TODO Appendix C: Alternative Backends
- TODO FAQ
- Glossary of terms, functions, macros, acronyms
Hoplon is a ClojureScript library for developing Single-page Applications.
In particular, Hoplon applications are usually made up of two things: components and cells
-
components are ClojureScript functions that return other components
or DOM nodes and are composed to form user interfaces.
- control how the application looks at any one time
- delivered by the hoplon/hoplon library
-
cells are ClojureScript reference types that are composed to perform
transactional state transitions in response to user or server-sent events.
- control how the application looks and behaves over time
- delivered by the hoplon/javelin library
This document is intended to convey enough information about Hoplon for you to make a decision about whether or not to use it, and to get you started if you do.
If you are assessing Hoplon, in addition to reading this document you may consider setting your computer up for Hoplon development. It takes about 10 minutes.
Otherwise, you can skip ahead to Components to learn more about Hoplon without setting up a development environment.
Follow the instructions below to set up your computer to develop Hoplon applications. Windows, MacOs and Linux will all work fine.
https://clojure.org/guides/install_clojure
https://nodejs.org/en/download
Install deps-new if you haven't already:
clojure -Ttools install-latest :lib io.github.seancorfield/deps-new :as new
And then generate a starter project with:
clojure -Sdeps '{:deps {io.github.hoplon/project-template {:git/tag "v1.0.0" :git/sha "14361f1"}}}' -Tnew create :template hoplon/hoplon :name example/address-book
The above command creates an address-book
directory wherever you ran it. The directory has the following structure:
address-book/
├── LICENSE
├── package.json
├── public
│ └── index.html
├── README.md
├── shadow-cljs.edn
└── src
└── example_project
├── main.cljs
└── view.cljs
Enter the directory.
Install node dependencies (you will only need to do this again if you change
your npm dependencies on package.json
:
npm install
Then run the build tool in development mode:
npx shadow-cljs watch app
Open http://localhost:8000 in your browser. Then, open src/example_project.view.cljs
in
your favorite text editor. You should see the page reload automatically when you
save changes.
User-defined components and the browser's native DOM nodes are aligned through a set of conventions we call HLisp.
For example, this HTML fragment:
<span class="urgent">
<a href="http://www.zombo.com/">Zombo</a>
</span>
can be expressed in ClojureScript using HLisp conventions as:
(h/span :class "urgent"
(h/a :href "http://www.zombon.com/" "Zombo"))
In the example above:
-
h/span
andh/a
are function calls to constructor functions defined in hoplon.core. - Constructor functions parse their arguments for attribute pairs and children, and apply the attribute values and append child nodes to the elements they return.
- Strings like
"Zombo"
are parsed as DOM text nodes and appended to their parent element.
To simplify the programmatic creation of nodes, HLisp also supports map syntax for attributes:
(h/span {:class "urgent"}
(h/a {:href "http://www.zombon.com/"} "Zombo"))
While HLisp-style ClojureScript and HTML files describe the same thing - the structure of a document or document fragment - HLisp is not an alternative HTML syntax. That is, HLisp is not converted to HTML. It is converted ultimately to JavaScript, via ClojureScript.
As such, HTML files in Hoplon projects usually only serve the purpose of including and loading JavaScript. They are the entrypoint.
Custom components in Hoplon are functions that return other custom components or native browser elements.
(defn say-hello [msg]
(h/p msg))
(say-hello "Hello world")
Here, say-hello
is user-defined function that takes a string argument and
returns a p
node with a single child, the text node of msg
.
Because components are functions, they can be passed to higher-order functions like map
:
(defn make-list [& items]
(h/ol (map h/li items)))
(make-list "one" "two" "three")
In this example:
-
make-list
takes any number ofitems
, which should be strings - The expression
(map h/li items)
maps theh/li
constructor function over each string, resulting in a sequence ofli
nodes - Each
li
produced by themap
expression is appended to a newol
and theol
is returned. This demonstrates the HLisp convention that sequence arguments to constructor functions are appended to the resulting element.-
apply
could also be used to construct anol
with children:(apply h/ol (map h/li items))
-
make-list
could be extracted to a .cljs
file and referenced from
other files by putting it in a file called src/app/components.cljs
:
(ns app.components
(:require [hoplon.core :as h]))
(defn make-list [& items]
(h/ol (map h/li items)))
src/my/app.clj
becomes:
(ns my.app
(:require [app.components :as c]))
(h/defn page
[]
(h/div (c/make-list "one" "two" "three")))
Medium to large-sized Hoplon applications typically consist of a small number of
files with components mounted directly in html files and a set of component
libraries in .cljs
files.
Component libraries can also be extracted to separate Maven artifacts and distributed via Clojars or some other Maven repository.
The Hoplon-provided component constructors like h/p
and h/a
can receive
attributes either as a map, or as a sequence of attribute name-value pairs
succeeded by any number of child components.
This functionality is extended to user-defined custom components using the h/defelem
macro for defining components.
For example, consider this updated version of make-list
(hoplon.core
is abbreviated as h
):
(defn make-list [attrs & items]
(h/ol attrs (map h/li items)))
When it's called like this:
(make-list {:class "big"} "one" "two" "three")
It evaluates to a DOM structure like this:
<ol class="big">
<li>one</li>
<li>two</li>
<li>three</li>
</ol>
But it doesn't work when it's called like this:
(make-list :class "big" "one" "two" "three")
The problem is that the attribute :class
is taken as the attrs
argument,
which needs to be a map in order to be passed to h/ol
as an attribute map. The
class name, "big"
, is taken as a child text node, not as the value of the
:class
attribute.
We can define make-list
using defelem
so that attributes and childen are
properly parsed, and provided as the arguments attrs
and kids
inside the
function body:
(h/defelem make-list [attrs kids]
(h/ol attrs (map h/li items)))
defelem
is a macro defined in the hoplon.core Clojure
namespace.
By themselves, components are static. That is, they don't change in response to events from the user or server. Without reactivity, components described with HLisp offer no benefit over regular markup such as that found in HTML files.
Cells provide a way to model changes in data in a SPA in response to events. Cells can be wired to components, and so can be used to dynamically modify the attributes or children associated with a component.
Cells are:
- reference types like atoms
- provided by the hoplon/javelin library
- similar to the cells in spreadsheets; automatically and consistently propagate data dependency changes
- the way interactivity and reactivity in Hoplon applications are modeled
- not tied to the other parts of Hoplon; can be used in any ClojureScript project
This section will not be a comprehensive explanation of the Javelin library. Instead, we will introduce Javelin cells and then focus mostly on the relationship between cells and components in a Hoplon SPA. For a thorough introduction to Javelin you can check out the Javelin README.
In a regular .cljs
file, you can include Javelin using something like this:
(ns your-ns
(:require [javelin.core :refer [cell cell=]]))
The javelin.core/cell
function is used to create new "input" cells.
Input cells can do everything ClojureScript's native atoms can do:
; Create a cell named x with an initial value of 0
(def x (cell 0))
; Increment the value of x
(swap! x inc)
; Set the value of x to 0
(reset! x 0)
; Print the new value of x every time it changes
(add-watch x :log (fn [_ _ _ v] (.log js/console v)))
In addition, input cells can contribute to the value of formula cells, which we'll get into shortly.
In practice, input cells are used to "collect" the values of events coming from the user or server, such as clicks, key presses, or updates.
In the callback functions for events, new values are saved to one or more input
cells with swap!
or reset!
.
The practice of putting the value associated with an event into a cell in response to an event is an alternative to calling more functions or firing more events. Cells provide an important level of indirection that promotes a value-oriented — not event-oriented — way of modeling an application's behavior.
Instead of an application's behavior being defined directly by the order in which events are fired, the behavior depends directly on values contained in cells.
The use of cells to model behavior is the primary means by which Hoplon applications mitigate callback hell.
In the following example, the counter
cell is incremented, and
its value printed to the JavaScript console every time the button is clicked:
(ns my.example
(:require
[hoplon.core :as h]
[javelin.core :refer [cell]]))
(def counter (cell 0))
(add-watch counter :log (fn [_ _ _ v] (.log js/console v)))
(h/defelem view []
(h/div
(h/button :click #(swap! counter inc) "Increment!")))
Hoplon HTML constructor functions can take any jQuery event keyword and a callback as an attribute name/value pair. We leverage this in the above example to attach an anonymous callback that swaps
counter
every time the button's:click
event is fired.
In this most simple example, cell
could be substituted for atom
and the
behavior would be identical. To really take advantage of cells, we must
introduce formula cells.
Formula cells are like input cells and atoms in that they contain values. Unlike
input cells and atoms, their values cannot be directly modified with a call to
swap!
or reset!
.
Instead, the value of a formula cell is given by the continuous application of a function to other cells. That is, a formula cell's value is the return value of a function that's applied whenever its arguments change.
There is a hybrid formula/input cell, called a "lens" in Javelin parlance, which is something like a formula that can be modified. It's a kind of cell that specifies both how a value should be computed and how the input cells contributing to the value should be modified.
Lenses are also created with the
formula
function. For more on lenses, check out the Javelin README.
Here, we augment the counter example by adding a new formula cell, even
. It
contains true
or false
, depending on whether or not counter
is even:
(ns my.example
(:require
[hoplon.core :as h]
[javelin.core :refer [cell formula]]))
(def counter (cell 0))
(def even ((formula even?) counter))
(add-watch counter (fn [_ _ _ v] (.log js/console v)))
(add-watch even (fn [_ _ _ v] (.log js/console v)))
(h/defelem view []
(h/div
(h/button :click #(swap! counter inc) "Increment!")))
The even
cell is created with the formula
function like this:
-
(formula even?)
returns a function that should be passed the same number of arguments aseven?
. -
((formula even?) counter)
returns a formula cell, the value of which is governed by the continuous application ofeven?
tocounter
's value. -
(def even ((formula even?) counter))
assigns the formula cell to the global nameeven
.
As a reference type like input cells and atoms, formula cells created with
formula
can be dereferenced at any time.
The following code snippet would print the value of even
, once, to the console:
(.log js/console @even)
Note: Dereferencing cells is useful when performing side-effects in callback functions, but in general should be avoided.
In the typical Hoplon application, cells form the "data layer" and components form the "view layer".
Events produced by components contribute new values to cells, and in turn zero or more components are updated.
This flow of activity can be thought of abstractly as a loop, but concretely, it is not, because the occurrence of an event does not necessarily result in the changing of a value in a cell.
Because the loop, in practice, is broken by one or more "event gaps", we can consider the data flow unidirectional.
Note: Javelin/cells make no effort to detect cyclical cell relationships. It's entirely possible to create cycles — and freeze the browser — by linking cells in a loop through a pair of add-watch calls. This almost never happens if you've sufficiently avoided dereferencing cells (see above) but is useful to know about regardless.
We've already seen how event callbacks can modify input cell values in their callbacks. In this section we will examine how the values of cells are "mapped" to components.
In particular, we will examine the three ways a cell value can influence the way a component looks:
- Setting attributes
- Appending text, a special case of appending children
- Appending children components
HTML attributes and JS events (on!, do!)
// Show text macro
// for-tpl
- Routing?
- Authentication?
Term | Definition |
---|---|
cell |
javelin.core/cell , function, creates a new input cell: (def n (cell 0))
|
cell= |
javelin.core/cell= , macro, creates a new formula cell (def n+1 (cell= (inc n)))
|
component | A ClojureScript function that returns an HTML element that can be embedded in a page. Used to extend HTML by modularizing behaviors and abstracting over other components and HTML elements. |
component constructor | Another term for "component", above. |
ClojureScript | The ClojureScript language and compiler, a dialect of Clojure that targets the JavaScript platform. .cljs.hl files are compiled by the hoplon task into .cljs files that are converted into JavaScript by the ClojureScript compiler. |
defelem |
hoplon.core/defelem , macro, convenience macro for creating components, or functions that return HTML objects. Handles "boxing" attributes and children. e.g. (defelem example [attrs kids] {:attrs attrs, :kids kids}) when called like (example :foo 1 :bar 2 (p "lol")) returns {:attrs {:foo 1, :bar 2}, :kids [#<[object HTMLParagraphElement]>]}
|
defc |
javelin.core/defc , macro, creates a new input cell (defc n 0)
|
defc= |
javelin.core/defc= , macro, creates a new formula cell (defc= n+1 (inc n))
|
formula |
javelin.core/formula , function, converts "normal" functions into functions of cells that participate in Javelin's graph of cells. e.g. (cell= (println x)) is equivalent to ((formula println) x) . cell= compiles into ClojureScript code that calls the formula function. formula can be used from JavaScript. |
Hoplon | The Hoplon Clojure and ClojureScript web framework |
Javelin | The Javelin ClojureScript library bundled with Hoplon |
SPA | "Single-page Application", a browser-based application such as GMail that leverages the web browser as a rich client instead of as a hyperlinked document viewer. Popular alternative to desktop apps since around when XMLHttpRequest/Ajax techniques entered the mainstream in 2007. Hoplon was built specifically to build SPAs. |